Android/App개발2014.07.16 13:31


http://developer.android.com/guide/practices/screens-distribution.html

모든 스크린사이즈에 적절하게 표시되도록 화면을 디자인할것을 권고하지만 태블릿만, 폰만 또는 특정크기이상의 스크린만 지원하고자 하는 경우가 있다. 그러려면 지원할 스크린에 대한 설정을 manifest에 추가하여 구글플레이같은 외부서비스에 의해 필터링이 되도록 할 수 있다.

그전에 멀티스크린지원에 대한 정확한 이해가 필요하고 그에 따라 구현해야 한다.

폰만 지원하기

일반적으로 큰스크린에 잘 맞도록 시스템이 동작하기 때문에 큰스크린에 대한 필터는 필요치 않다. Best Practies for Screen Independence에 잘 따랏다면 태블릿에서 아주 잘 동작할것이다.  하지만 스케일업이 생각처럼 동작하지 않는것을 발견할 수 있으며 애플리케이션을 다른 스크린설정으로 두개의 버전을 배포하려고 할 수 있다. 이 경우 <compatible-screens> 로 screen size와 density의 조합으로 조정할 수 있다. Google Play는 이 정보를 참조하여 애플리케이션을 필터링한다. 그리하여 단말에 적합한경우에만 다운로드 설치가 가능하도록 한다.

<screen> 엘리먼트를 포함해야 하는데 각 <screen>은 스크린설정정보(스크린크기, 스크린밀도)를 담는다. 두가지(screenSize, screenDensity)정보를 모두 담아야 한다. 하나라도 빠져있다면 Google Play는 그 필드를 무시할것이다.

만일 small, normal size screen을 지원하고 density는 무시하고자 한다면 총 8개의 <screen> 엘리먼트가 필요하다. 각 사이즈별로 density가 4가지이기 때문이다. 여기서 정의되지 않은 조합은 호환되지 않는것으로 간주된다. 다음은 이 경우에 대한 manifest예이다.

<manifest ... >

    <compatible-screens>

        <!-- all small size screens -->

        <screen android:screenSize="small" android:screenDensity="ldpi" />

        <screen android:screenSize="small" android:screenDensity="mdpi" />

        <screen android:screenSize="small" android:screenDensity="hdpi" />

        <screen android:screenSize="small" android:screenDensity="xhdpi" />

        <!-- all normal size screens -->

        <screen android:screenSize="normal" android:screenDensity="ldpi" />

        <screen android:screenSize="normal" android:screenDensity="mdpi" />

        <screen android:screenSize="normal" android:screenDensity="hdpi" />

        <screen android:screenSize="normal" android:screenDensity="xhdpi" />

    </compatible-screens>

    ...

    <application ... >

        ...

    <application>

</manifest> 


주의) 하지만 2014년 6월 현재 하나가 더 있다. xxhdpi 따라서 이제 10개가 필요하게 되겠다.

Note) <compatible-screens>엘리먼트는 리버스를 통해 호환 여부를 작은사이즈만 지원하는지 알수 있으나 <supports-screens>를 사용하면 density를 요구하지 않기 때문에 피할 수 있다.


태블릿만 지원하기

폰을 지원하지 않겠다면(즉 큰스크린만 지원하겠다면) 또는 작은 스크린에 대해 최적화하는데 시간이 필요하다면 작은스크린 디바이스는 설치안되도록 할 수 있다. 이 방법은 <support-screens>를 통해 가능하다.

<manifest ... >

    <supports-screens android:smallScreens="false"

                      android:normalScreens="false"

                      android:largeScreens="true"

                      android:xlargeScreens="true"

                      android:requiresSmallestWidthDp="600" />

    ...

    <application ... >

        ...

    </application>

</manifest>

두가지 방법이 있다.

  • "small"과 "normal"사이즈를 미지원으로 설정, 일반적으로 이런 디바이스는 태블릿이 아니다
  • 앱이 허용하는 영역이 최소 600dp이상으로 설정

첫번째는 android 3.1과 그 이 하의 디바이스들을 위한것이다. 왜냐하면 그 디바이스들은 일반화된 스크린사이즈에 기반하여 사이즈를 정의하였기 때문이다. requiresSmallestWidthDp속성은 android 3.2와 그이상을 위한것이다. 이는 dip의 최소값에 기반하여 요구되는 사이즈를 정할 수 있다. 예를 들면 600dp로 설정하면 일반적으로 7인치 이상의 스크린을 갖는 디바이스들이 해당된다.

당신이 필요로 하는 사이즈는 당현히 다를수 있다. 만인 9인치 이상의 스크린을 지원하고자 한다면 720dp로 설정할 수 있을 것이다.

앱은 requiresSmallestWidthDp속성을 빌드하기 위해 Android 3.2이상으로 빌드를 해야 한다. 이전버전은 에러가 날것이다. 가장 안전한 방법은 필요로 하는 API level에 맞게 minSdkVersion을 설정하는 것이다. 최종배포 질드시 빌드타겟을 3.2로 변경하고 requeiresSmallestWidthDp를 추가한다. 3.2이전버전은 런타임에 이 값을 무시할것이기 때문에 문제가 없을 것이다.

왜 가로사이즈만으로 스크린을 구분하는지 궁금하면 다음 링크를 참조해라. -> New Tools for Managing Screen Sizes.

여러화면을 지원하기 위해 필요한 모든 기술을 적용하여 가능한 많은 장치에 앱을 사용할 수 있도록 하기 위해 <compatible-screens>를 사용하거나 모든 화면구성에 호환성을 제공할 수 없을 경우에만 <support-screens>을 사용하거나 또는 특정한 화면구성의 다른 세트에 대한 다른 버전의 앱을 제공할 수 있다.


다른 화면을 위한 여러개의 APK제공

하나의 APK만을 제공하는것이 권장사항이긴 하지만 구글플레이는 여러 화면설정에 따라 여러개의 APK를 동일 앱에 대하여 제공하는것을 허용한다. 예를 들어 폰과 태블릿버전을 모두 제공하고자 하는데 동일 APK로 만들수 없는 경우에 실제로 2개의 APK를 게시할 수 있다. 구글 플레이는 디바이스 화면에 맞춰서 APK를 배포할것이다.

하지만 하나로 게시하는제 좋을거다. 라는게 구글의 여전한 권장사항이다. 

더 자세한 정보를 원하면 다음 링크를 더 살펴봐라. Multiple APK Support.



Posted by 삼스
Android/App개발2014.05.21 21:57


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;
    }


    Posted by 삼스
    Android/App개발2013.11.06 19:28



    Android 3.1 부터 system 의 package manager 는 application 의 stop 상태를 유지관리하며, background process 나 다른 app 에서 launch 할 수 있는 것을 control 합니다. 


    App 실행후 Back Key를 이용하여 App을 종료한 상태와, 

    단말기 "환경설정 > 에플리케이션 관리 > App > 앱 강제 종료"와는  상태가 다르다는 것 입니다.



    여기서 Back Key를 이용해 Activity를 종료하는 것은 stopped state이며 앱 강제 종료는 Application Stopped State 입니다.


    문제는 안드로이드 3.1이전에 사용한 방식으로 BroadCast 메시지를 발송하였을때 Application Stopped State 상태에서는 Broadcast 메시지를 받을 수 없습니다.


    Application Stopped State에서 BroadCast 메시지를 받기위해서는 FLAG_INCLUDE_STOPPED_PACKAGES를 추가하여 Broadcast하여야 합니다. 



     FLAG_INCLUDE_STOPPED_PACKAGES  : stopped 된 application 도 target 이 됩니다. 

     FLAG_EXCLUDE_STOPPED_PACKAGES : stopped 된 application 은 target 이 되지 않습니다. 

    * 기본 값은 FLAG_EXCLUDE_STOPPED_PACKAGES 입니다.



    사용 방법

    1
    2
    3
    4
    5
    6
    7
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
         intent.addFlags(FLAG_INCLUDE_STOPPED_PACKAGES);
    }
      
    sendBroadcast(intent);


    그렇담.. 

    Push리시버같은것은 어떻게 해야 할까요???


    Posted by 삼스
    Android/App개발2013.07.11 19:13



    private void getMusicMetaInfo(String fileStr) {

    Uri fileUri = Uri.parse(fileStr);

    String filePath = fileUri.getPath();

    Cursor c = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,null,"_data ='" + filePath + "'",null,null);

    c.moveToNext();

    if(c.getCount()>0) {

    int id = c.getInt(0);

    Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,id);

    Log.d(LOG_TAG, fileStr + "'s uri : " + uri.toString());

    Cursor cur = managedQuery(uri, null, null, null, null);

    if(cur.moveToFirst()) {

    Log.d(LOG_TAG, "==============================");


    // int artistColumn = cur.getColumnIndex(MediaStore.Audio.AlbumColumns.ARTIST);

    Log.d(LOG_TAG, "타이틀 : " + cur.getString(cur.getColumnIndex(MediaStore.Audio.AudioColumns.TITLE)));

    Log.d(LOG_TAG, "아티스트 : " + cur.getString(cur.getColumnIndex(MediaStore.Audio.AlbumColumns.ARTIST)));

    Log.d(LOG_TAG, "앨범 : " + cur.getString(cur.getColumnIndex(MediaStore.Audio.AlbumColumns.ALBUM)));

    Log.d(LOG_TAG, "재생시간 : " + cur.getString(cur.getColumnIndex(MediaStore.Audio.AudioColumns.DURATION)));


    Log.d(LOG_TAG, "==============================");

    }

    }

    c.close();

    Log.d(LOG_TAG, "");

    }

    Posted by 삼스
    Android/App개발2013.03.07 11:20


    안드로이드 Bluetooth 프로그래밍을 위해 http://developer.android.com/guide/topics/connectivity/bluetooth.html의 내용을 정리함.

    안드로이드 플랫폼은 블루투스네트웍스택을 제공하여 다음의 블루투스의 기능을 제공한다.

    - 다른 디바이스의 스캔

    - 페어링을 위해 로칼디바이스에 쿼리 

    - RFCOMM채널 연결 가능

    - 서비스 디스커버리를 통한 타디바이스 연결

    - 디바이스간 데이터 전송

    - 다중연결 관리


    기초

    블루투스셋업, 페어링된 또는 유효한 디바이스 찾기, 연결하기 그리고 데이터 전송하기에 대한 기초적인 내용을 기술하겠다.

    android.bluetooth패키지에 모든 API들이 정의되어 있다. 다음에 설명되는 클래스들이 대표적인 클래스들이다.

    BluetoothAdapter

      로컬블루투스어댑터를 대표한다. 이 객체를 통해서 디바이스를 찾고 연결하고 디바이스를 활성화하여 통신을 수행하는등의 작업이 가능하다.

    BluetoothDevice

      원격블루투스디바이스를 대표한다. 이 객체를 통해 블루투스소켓을 통한 연결이나 디바이스의 속성정보를 요청할 수 있다.

    BluetoothSocket

      블루투스소켓의 인터페이스를 대표한다. TCP socket과 유사하다. 입출력스트림을 통해 다른 디바이스와 데이터를 주고받을 수 있다.

    BluetoothServerSocket

      들어오는 요청을 수신할 수 있는 서버소캣을 대표한다. TCP ServerSocket과 유사하다. 

    BluetoothClass

      블루투스디바이스의 특성과 사양을 나타낸다. 읽기전용속성이며 major, minor 디바이스 클래스로 디바이스의 성격을 규정한다.  

    BluetoothProfile

      블루투스프로파일을 나타낸다. 블루투스기반의 통신을 위한 무선인터페이스 규격을 말한다. 예를 들어 핸즈프리 프로파일같은 것을 말한다. 더 자세한 내용은 http://developer.android.com/guide/topics/connectivity/bluetooth.html#Profiles 를 참조하라.

    BluetoothHeadset

      모바일폰을 위한 블루투스헤드셋을 지원한다. 핸즈프리(v1.5)프로파일을 포함한다.

    BluetoothA2dp

      오디오전송을 위한 품질을 정의한다. A2DP(Advanced Audio Distribution Profile)

    BluetoothHealth

      건강관련 디바이스 프로파일

    BluetoothHealthCallback

      BluetoothHealth 콜백을 구현하기 위한 추상클래스. 애플리케이션의 등록상태나 블루투스채널의 변경이나 업데이트의 수신을 감지하기 위해 이 클래스를 반드시 구현해야 한다.

    BluetoothHealthAppCOnfiguration

      Bluetooth Health 3rd-party애플리케이션을 health device에 연결하기 위한 설정

    BluetoothProfile.ServiceListener

      Bluetooth 서비스가 연결 또는 차단되었을 때 BluetoothProfile IPC 클라이언트에 통지를 한다. 즉 내부에서 서비스는 각각의 프로파일로 동작한다.


    블루투스 권환

    BLUETOOTH와 BLUETOOTH_ADMIN 권한이 준비되어 있다.

    BLUETOOTH : 블루투스통신을 위한 권한

    BLUETOOTH_ADMIN :  디바이스 디스커버리나 장치설정등을 할 수 있는 권한.


    블루투스 셋업

    블루투스를 사용하기 전에 디바이스가 블루투스가 가능한지 살펴야 한다.

    1. BluetoothAdapter를 얻어낸다.

    1. BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
      if (mBluetoothAdapter == null) {
         
      // Device does not support Bluetooth
      }

    2. 블루투스를 활성화한다.

    if (!mBluetoothAdapter.isEnabled()) {
       
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult
    (enableBtIntent, REQUEST_ENABLE_BT);
    }

    위와 같이 호출하면 블루투스활성화여부 팝업이 뜨며 사용자가 확인을 선택하면 RESULT_OK가 onActivityResult()에서 리턴되고 그러지 않으면 RESULT_CANCELED가 리턴된다.

    이 때 앱은 onPause와 onResume루틴을 타게 되니 유의한다.


    디바이스 찾기

    BluetoothAdapter를 통해 근처의 디바이스들을 찾을 수 있으며 페어링된 다바이스리스트도 얻을 수 있다.

    디바이스디스커버리는 주변의 블루투스가 활성화되어 있는 디바이스들을 스캔한다. 그리고 각 디바이스의 정보를 수집한다. 수집이 가능한 디바이스는 블루투스가 활성화되어 있고 검색가능한 상태로 셋팅되어 있어야 한다. 그러면 검색되어 지고 디바이스명, 클래스 그리고 MAC주소가 수집이 가능하다. 이 정보를 기반으로 특정 디바이스를 선택하여 연결할 수 있다.

    한번 연결이 이루어진 디바이스는 자동으로 페어링이 요청되게 된다. 디스커버리할때 저장된 MAC주소정보를 통해 디서커버리 단계 없이 바로 연결을 할 수 있다.

    페어링과 연결은 한가지 차이점이 있다. 페어링은 두 디바이스가 서로 존재함을 인식하는 것이고 인증을 위해 공유된 link-key를 가지고 서로 암호화된 연결을 확립할 수 있다.

    연결은 디바이스간에 RFCOMM채널을 공유하였다는 것이고 서로 데이터를 주고 받을 수 있다는 것이다. 페어링이 먼저 되어야 통신을 수행할 수 있다.

    안드로이드가 탑재된 제품들은 discoverable이 꺼진 상태이다. 따라서 페어링을 위해 discoverable을 활성화하여야 한다. http://developer.android.com/guide/topics/connectivity/bluetooth.html#EnablingDiscoverability 를 참조하라.


    페어링된 다바이스 얻기

    디스커버리를 수행하기 전에 현재 페어링된 디바이스를 알아낼수 있다. getBondedDevices()메서드로 페어링된 디바이스들의 리스트를 얻을 수 있다.

    Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
    // If there are paired devices
    if (pairedDevices.size() > 0) {
       
    // Loop through paired devices
       
    for (BluetoothDevice device : pairedDevices) {
           
    // Add the name and address to an array adapter to show in a ListView
            mArrayAdapter
    .add(device.getName() + "\n" + device.getAddress());
       
    }
    }

    검색된 디바이스에는 연결을 위한 MAC주소정보가 포함되어 있으며 연결하기는 다음을 참조하라

    http://developer.android.com/guide/topics/connectivity/bluetooth.html#ConnectingDevices


    디바이스 디스커버리

    startDiscovery()메서드를 수행하면 된다. 프로세스는 비동기로 동작하고 메서드는 바로 리턴된다. 

    비동기로 진행되는 과정에 디바이스가 검색이 되면 ACTION_FOUND 인텐트가 브로드캐스트 되며 이 이벤트를 받기 위해 BroadcastReceiver를 사용해야 한다.

    // Create a BroadcastReceiver for ACTION_FOUND
    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
       
    public void onReceive(Context context, Intent intent) {
           
    String action = intent.getAction();
           
    // When discovery finds a device
           
    if (BluetoothDevice.ACTION_FOUND.equals(action)) {
               
    // Get the BluetoothDevice object from the Intent
               
    BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
               
    // Add the name and address to an array adapter to show in a ListView
                mArrayAdapter
    .add(device.getName() + "\n" + device.getAddress());
           
    }
       
    }
    };
    // Register the BroadcastReceiver
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    registerReceiver
    (mReceiver, filter); // Don't forget to unregister during onDestroy

    디스커버리 과정은 전원을 많이 소비사는 과정이므로 검색이 다 끝나면 반드시 cancelDiscovery()를 호출해주어야 한다. 그리고 연결이 되어 있는 상태에서 디스커버리는 통신데이터 bandwidth를 현저하기 떨어뜨리기 때문에 연결중에는 디스커버리를 하지 않아야 한다.

    디스커버러블 활성화

    ACTION_REQUEST_DISCOVERABLE 액션인텐트를 startActivityForResult로 호출한다. EXTRA_DISCOVERABLE_DURATION 으로 탐색시간을 파라메터로 넘길 수 있다. 최대시간은 3600초이고 0이하는 내부적으로 120초로 자동 셋팅된다. 다음은 300초로 셋팅한 예이다.

    Intent discoverableIntent = new
    Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
    discoverableIntent
    .putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
    startActivity
    (discoverableIntent);

    디바이스가 블루투스가 활성화가 안되어 있다면 자동으로 활성화후 진행한다.

    디스커버러블모드는 백그라운드에서 지정한 시간만큼 유지되었다가 끝나게 된다. 만일 이 상태변화를 처리하고 싶다면 ACTION_SCAN_MODE_CHANGED인텐트의 리시버를 통해서 처리하면 된다. 이 인텐트는 추가정보로 EXTRA_SCAN_MODE, EXTRA_PREVIOUS_SCAN_MODE로 현재와 이전 스캔모드를 얻을 수 있다. 

    모드는 3가지이다.

    - SCAN_MODE_CONNECTABLE_DISCOVERABLE :    디바이스가 둘다 가능한 상태

    - SCAN_MODE_CONNECTABLE :  디스커버러블모드는 아니고 연결은 가능한 상태.

    -  SCAN_MODE_NONE : 둘다 안되는 상태

    디바이스와 이미 연결이 초기화된 상태이면 굳이 디스커버러블을 활성화할 필요는 없다. 디스커버러블을 활성화하는 경우는 앱이 서버소켓을 제공하여 연결시도를 허락하기 위한 경우만 해당된다. 왜냐하면 원격디바이스들은 연결이 가능해지기 이전에 먼저 검색이 가능해야 하기 때문이다.


    디바이스 연결

    앱이 두단말간에 연결을 생성하기 위해서 서버와 클라이언트단을 모두 구현해야 한다. 하나는 서버가 되어서 대기하고 하나는 연결을 시도해야 하기 때문이다. 연결시 사용하는 정보는 서버의 MAC주소가 된다. 서버와 단말이 둘다 동일 RFCOMM채널상에서 BluetoothSocket으로 연결되면 상호간에 연결된것으로 간주된다. 이 때 입출력스트림을 통해 데이터를 주고받을 수 있다. 

    서버와 클라이언트 디바이스는 각각 다른 방식으로 요구되는 BluetoothSocket을 얻는다. 서버는 요청받은 연결을 허락했을 때 얻어지고 클라이언트는 서버와 RFCOMM채널이 열렸을 때 얻어진다.

    두 단말이 모두 자동으로 서버모드로 대기하고 서로간에 연결을 시도하는 방식과 명시적으로 하나는 호스트로써 서버로 대기하고 다른 하나가 연결을 시도하는 방식이 있다.

    두 디바이스가 페어링된적이 없으면 안드로이드는 자동으로 페어링요청 통지나 팝업을 연결과정에 띄운다. 그래서 연결시도과정에 두 단말이 페어링된적이 있는지 알 필요가 없다. 연결시도는 페어링과정이 끝난 이후 계속 진행된다.


    서버 연결

    BluetoothServerSocket으로 서버로 동작시킬 수 있다. 서버소켓은 연결요청을 받아들이기 위한것이다.

    1. BluetoothServerSocket을 얻는다.

       lisetnUsingRfcommWithServiceRecord(String, UUID)로 얻는다. String는 서비스명이고 UUID는 클라이언트디바이스와 연결 동의에 기반으로 사용된다. 다시 말해 클라이언트디바이스가 연결을 시도할 때 UUID가 전달되어진다. 다음단계에서 UUID는 서로 맷치가 되어야 한다.

    2. accept()메서드로 연결요청을 대기한다.

      연결이 허용되거나 예외가 발생할 때 까지 이 메서드는 블럭된다.  클라이언트의 연결요청이 UUID가 일치하였을 때만 accept가 성공하고 BluetoothSocket을 리턴한다.

    3. 더이상 연결을 허용하지 않을거라면 close()를 호출한다.

      서버소켓을 종료하고 모든 리소스를 해재한다. 연결된 상태는 유지된다.  TCP/IP와 다르게 RFCOMM은 하나의 채널에 하나의 연결만 허용한다. 따라서 accept()로 연결이 성립되면 서버소켓을 close()로 닫아준다.

    accept는 Main activity에서 호출하지 않아야 블럭되는것을 방지할 수 있다. 블럭중에 중단하려면 다른 스레드에서 close()를 호출하면 된다.

    다음은 서버구현의 예이다.

    private class AcceptThread extends Thread {
       
    private final BluetoothServerSocket mmServerSocket;
     
       
    public AcceptThread() {
           
    // Use a temporary object that is later assigned to mmServerSocket,
           
    // because mmServerSocket is final
           
    BluetoothServerSocket tmp = null;
           
    try {
               
    // MY_UUID is the app's UUID string, also used by the client code
                tmp
    = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
           
    } catch (IOException e) { }
            mmServerSocket
    = tmp;
       
    }
     
       
    public void run() {
           
    BluetoothSocket socket = null;
           
    // Keep listening until exception occurs or a socket is returned
           
    while (true) {
               
    try {
                    socket
    = mmServerSocket.accept();
               
    } catch (IOException e) {
                   
    break;
               
    }
               
    // If a connection was accepted
               
    if (socket != null) {
                   
    // Do work to manage the connection (in a separate thread)
                    manageConnectedSocket
    (socket);
                    mmServerSocket
    .close();
                   
    break;
               
    }
           
    }
       
    }
     
       
    /** Will cancel the listening socket, and cause the thread to finish */
       
    public void cancel() {
           
    try {
                mmServerSocket
    .close();
           
    } catch (IOException e) { }
       
    }
    }


    클라이언트 연결

    리모트디바이스와 연결하기 위해서는 일단 리모트디바이스객체를 얻어야 한다. 페어링된 디바이스를 얻거나 디스커버리 과정을 통해서 연결하고자 하는 BluetoothDevice를 얻어야 한다.

    1. BluetoothDevice.createRfcommSocketToServiceRecord(UUID) 를 호출

      BluetoothSocket을 초기화한다. 이 때 서버디바이스에서 제공하는 UUID를 인자로 넘긴다. 이 값이 동일해야 연결이 성립된다.

    2. connect()로 연결 시도

      UUID가 매칭되는 디바이스와 연결을 시도하고 성공하면 소켓을 리턴한다. 

    connect()메서드가 블럭되는 호출이기 때문에 분리된 스레드에서 수행되어야 한다.

    connect()과정 중에는 디스커버리가 진행되면 안된다. 만일 진행하게 되면 연결과정이 아주 느리게 진행되거나 실패하게 된다.

    다음은 간략한 예제이다.

    private class ConnectThread extends Thread {
       
    private final BluetoothSocket mmSocket;
       
    private final BluetoothDevice mmDevice;
     
       
    public ConnectThread(BluetoothDevice device) {
           
    // Use a temporary object that is later assigned to mmSocket,
           
    // because mmSocket is final
           
    BluetoothSocket tmp = null;
            mmDevice
    = device;
     
           
    // Get a BluetoothSocket to connect with the given BluetoothDevice
           
    try {
               
    // MY_UUID is the app's UUID string, also used by the server code
                tmp
    = device.createRfcommSocketToServiceRecord(MY_UUID);
           
    } catch (IOException e) { }
            mmSocket
    = tmp;
       
    }
     
       
    public void run() {
           
    // Cancel discovery because it will slow down the connection
            mBluetoothAdapter
    .cancelDiscovery();
     
           
    try {
               
    // Connect the device through the socket. This will block
               
    // until it succeeds or throws an exception
                mmSocket
    .connect();
           
    } catch (IOException connectException) {
               
    // Unable to connect; close the socket and get out
               
    try {
                    mmSocket
    .close();
               
    } catch (IOException closeException) { }
               
    return;
           
    }
     
           
    // Do work to manage the connection (in a separate thread)
            manageConnectedSocket
    (mmSocket);
       
    }
     
       
    /** Will cancel an in-progress connection, and close the socket */
       
    public void cancel() {
           
    try {
                mmSocket
    .close();
           
    } catch (IOException e) { }
       
    }
    }


    연결관리

    디바이스간 연결이 완료되면 BluetoothSocket을 각각 소유하게 된다. 이넘을 통해서 데이터를 주고받을 수 있다.

    1. 소켓으로부터 InputStream과 OutputStream을 얻는다.

    2.  read(byte[]), write(byte[])로 읽거나 쓰면 끝...

    두 메서드 모두 블럭되기 때문에 스레드로 분리되어야 한다.

    private class ConnectedThread extends Thread {
       
    private final BluetoothSocket mmSocket;
       
    private final InputStream mmInStream;
       
    private final OutputStream mmOutStream;
     
       
    public ConnectedThread(BluetoothSocket socket) {
            mmSocket
    = socket;
           
    InputStream tmpIn = null;
           
    OutputStream tmpOut = null;
     
           
    // Get the input and output streams, using temp objects because
           
    // member streams are final
           
    try {
                tmpIn
    = socket.getInputStream();
                tmpOut
    = socket.getOutputStream();
           
    } catch (IOException e) { }
     
            mmInStream
    = tmpIn;
            mmOutStream
    = tmpOut;
       
    }
     
       
    public void run() {
           
    byte[] buffer = new byte[1024];  // buffer store for the stream
           
    int bytes; // bytes returned from read()
     
           
    // Keep listening to the InputStream until an exception occurs
           
    while (true) {
               
    try {
                   
    // Read from the InputStream
                    bytes
    = mmInStream.read(buffer);
                   
    // Send the obtained bytes to the UI activity
                    mHandler
    .obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                           
    .sendToTarget();
               
    } catch (IOException e) {
                   
    break;
               
    }
           
    }
       
    }
     
       
    /* Call this from the main activity to send data to the remote device */
       
    public void write(byte[] bytes) {
           
    try {
                mmOutStream
    .write(bytes);
           
    } catch (IOException e) { }
       
    }
     
       
    /* Call this from the main activity to shutdown the connection */
       
    public void cancel() {
           
    try {
                mmSocket
    .close();
           
    } catch (IOException e) { }
       
    }
    }


    프로파일로 동작하기

    안드로이드 3.0부터 블루투스프로파일로 동작이 가능해졌다. 블루투스프로파일은 무선인터페이스 규격으로 블루투스기반의 디바이스간 통신에 기반한것이다. 예를 들어 헨즈프리프로파일같은 것이 있는데 모바일폰과 헤드셋간의 연결이 가능하려면 두 디바이스 모두 핸즈프리 프로파일을 지원해야 한다.

    블루투스프로파일을 직접 커스텀하게 작성이 가능하다. 안드로이드 블루투스 API는 다음의 프로파일의 구현을 지원한다.

    - 헤드셋

    - A2DP

    - Health Device

    프로파일기반으로 동작시키려면 다음과정으로 진행한다.

    1. 블루투스어댑터를 얻는다.

    2. 프로파일 프록시 객체를 얻는다 getProfileProxy(). 이 객체로 연결을 달성하게 된다. 아래 예제에서는 BluetoothHeadset의 인스턴스를 프록시객체로 사용하였다.

    3. BluetoothProfile.ServiceListener를 셋업하여 연결과 해재 이벤트를 처리한다.

    4. onServiceConnected()에서 프록시객체의 핸들을 얻는다.

    5. 프록시객체를 얻으면 이 객체를 통하여 연결상태를 모니터링하고 해당 프로파일과 연관된 동작들을 수행할 수 있다.

    다음은 BluetoothHeadset 프록시객체를 사용하여 해드셋프로파일을 제어하는 예이다.

    BluetoothHeadset mBluetoothHeadset;
     
    // Get the default adapter
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
     
    // Establish connection to the proxy.
    mBluetoothAdapter
    .getProfileProxy(context, mProfileListener, BluetoothProfile.HEADSET);
     
    private BluetoothProfile.ServiceListener mProfileListener = new BluetoothProfile.ServiceListener() {
       
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
           
    if (profile == BluetoothProfile.HEADSET) {
                mBluetoothHeadset
    = (BluetoothHeadset) proxy;
           
    }
       
    }
       
    public void onServiceDisconnected(int profile) {
           
    if (profile == BluetoothProfile.HEADSET) {
                mBluetoothHeadset
    = null;
           
    }
       
    }
    };
     
    // ... call functions on mBluetoothHeadset
     
    // Close proxy connection after use.
    mBluetoothAdapter
    .closeProfileProxy(mBluetoothHeadset);


    Posted by 삼스
    Android/App개발2012.08.14 08:59


    JellyBean(4.1) WebKit은 ICS대비 변경 없음

    ICS(4.0) Webkit

    • WebKit updated to version 534.30
    • Support for Indic fonts (Devanagari, Bengali, and Tamil, including the complex character support needed for combining glyphs) in WebView and the built-in Browser
    • Support for Ethiopic, Georgian, and Armenian fonts in WebView and the built-in Browser
    • Support for WebDriver makes it easier for you to test apps that use WebView

    HoneyComb(3.1) WebKit
    • File scheme cookies
      • The CookieManager now supports cookies that use the file: URI scheme. You can usesetAcceptFileSchemeCookies() to enable/disable support for file scheme cookies, before constructing an instance of WebView or CookieManager. In a CookieManager instance, you can check whether file scheme cookies is enabled by calling allowFileSchemeCookies().
    • Notification of login request
      • To support the browser autologin features introduced in Android 3.0, the new methodonReceivedLoginRequest() notifies the host application that an autologin request for the user was processed.
    • Removed classes and interfaces
      • Several classes and interfaces were removed from the public API, after previously being in deprecated state. See theAPI Differences Report for more information.


    HoneyComb(3.0) Webkit
    • New WebViewFragment class to create a fragment composed of a WebView.
    • New WebSettings methods:
      • setDisplayZoomControls() allows you to hide the on-screen zoom controls while still allowing the user to zoom with finger gestures (setBuiltInZoomControls() must be set true).
      • New WebSettings method, setEnableSmoothTransition(), allows you to enable smooth transitions when panning and zooming. When enabled, WebView will choose a solution to maximize the performance (for example, the WebView's content may not update during the transition).
    • New WebView methods:
      • onPause() callback, to pause any processing associated with the WebView when it becomes hidden. This is useful to reduce unnecessary CPU or network traffic when the WebView is not in the foreground.
      • onResume() callback, to resume processing associated with the WebView, which was paused during onPause().
      • saveWebArchive() allows you to save the current view as a web archive on the device.
      • showFindDialog() initiates a text search in the current view.
    안드로이드 버전별 WebKit버전
    3.0 - AppleWebKit/534.13
    2.3 - AppleWebKit/533.1
    2.2 - AppleWebKit/533.1
    2.1 - AppleWebKit/530.17
    

    Posted by 삼스
    Android/App개발2012.07.16 17:38


    출처 : http://iyeti.kr/630

    안드로이드를 개발하면서 아이폰과는 달리 가장 짜증났던부분이 해상도에 관련된 문제 일듯합니다.
    안드로이드 펍에서 회색님이 기본적인걸 정리해 주셨네요. 


    =============== 스마트폰 =================

    [HVGA 스마트폰]

    - RESOLUTION : 320px(320dp), 480px(480dp)

    - DENSITY DPI  : mdpi, 160 dpi

    - SCREEN SIZE : normal


    예 : G1, 옵티머스원


    [WVGA 스마트폰] 

    - RESOLUTION : 480px(320dp), 800px(533dp) or 854px(569dp)

    - DENSITY DPI  : hdpi, 240 dpi

    - SCREEN SIZE : normal


    예 : WVGA800 : 넥서스원, 갤럭시S, 갤럭시S2 WVGA854 : 모토로라 드로이드, XPERIA X10


    =============== 태블릿 =================

    [7인치 태블릿]

    - RESOLUTION : 600px(600dp), 1024px(1024dp)
    - DENSITY DPI  : mdpi, 160 dpi

    - SCREEN SIZE : large


    예 : 킨들 파이이, 누크 태블릿


    [10인치 태블릿]

    - RESOLUTION : 1280px(1280dp), 800px(800dp) or 720px(720dp)
    - DENSITY DPI  : mdpi, 160 dpi

    - SCREEN SIZE : xlarge


    예 : 갤럭시탭 10.1, 모토로라 XOOM


    =============== 태블릿폰 =================

    [갤럭시노트]

    - RESOLUTION : 800px(400dp), 1280px(640dp)

    - DENSITY DPI  : xhdpi, 320 dpi

    - SCREEN SIZE : ??? (Gingerbread)


    [7인치 갤럭시탭(2010)]

    - RESOLUTION : 600px(400dp), 1024px(682dp)

    - DENSITY DPI  : hdpi, 240 dpi

    - SCREEN SIZE : large(Gingerbread)



    크게 4가지로 나눠지네요. 

    1. 320dp 스마트폰 

    2. 400dp 태블릿폰 (2011 현재 삼성에서만 출시, 갤탭(2010)은 7인치지만 dp 분류상 갤노트와 같이 분류.)

    3. 600dp 7인치 태블릿

    4. 720dp(800dp) 10인치 태블릿. 


    * 참고 * 

    http://developer.android.com/guide/practices/screens_support.html

    http://android-developers.blogspot.com/2011/07/new-tools-for-managing-screen-sizes.html


    * 주의사항 *

    - 킨들파이어(2011) : 스크린 사이즈가 실제 디바이스에서는 Large인데 에뮬레이터에서는 XLarge로 잡힐 수 있습니다. 안드로이드 3.0 이하에서는 스크린 사이즈 잡는 부분이 문서와 같이 동작하지 않습니다. 


    - 갤럭시탭(2010) : 400dp라는 흔치 않은 넓이를 가지고 있으며 문서와 다르게 Normal이 아니라 Large로 잡습니다. 문서대로라면 ICS로 업그레이드시에 달라질수 있을것으로 보이니 유의해야할것 같습니다.



    실제기기에서 확인해보지 않은 값도 있습니다. 잘못된 부분 있으면 알려주세요. 확실치 않은 부분은 ? 붙여놓을테니 확인 부탁드립니다.


    위키에 좀더 자세한 해상도 정보가 있습니다. :  http://en.wikipedia.org/wiki/List_of_displays_by_pixel_density 

    [출처 : 
     http://www.androidpub.com/index.php?mid=android_dev_info&document_srl=1895935]


    #.2012.04.19 내용추가
    갤럭시넥서스: layout-sw360dp
    갤럭시노트: layout-xhdpi-1280x800 (or layout-normal-xhdpi)
    갤럭시탭10.1: layout-xlarge-mdpi(3.2이상 버전에서는 layout-sw800dp로도 가능)
    HD(1280x720)폰: layout-xhdpi-1280x720 (layout-normal-xhdpi)
    HTC Evo4g: layout-hdpi-960x540


    Posted by 삼스
    Android/App개발2011.10.20 19:06

    http://sdw8001.tistory.com/31
     

    이번에 정리해 볼 이슈는 안드로이드 개발에 반드시 사용하게 되는 에뮬레이터의 관한 이슈이다.
    에뮬레이터가 특별한 이슈가 될 것은 없지만 Network programming 을 할 때는 이슈가 발생한다.
    특히, 안드로이드 애플리케이션이 서버로 동작할 때는 더욱더 그러하다.

    들어가기에 앞서, 에뮬레이터를 클라이언트로 이용해서 사용하는 경우에는 별다른 이슈가 없다고 하였는데 이미 에뮬레이터에는 브라우저도 있고, WebView 클래스를 이용한 Webkit controller도 띄우는 예제가 있다. 다만, 외부로 접속하고자 할 경우(즉, 인터넷을 사용하는 경우에는) user-permission 셋팅을 해 주어야 하는 것을 잊지 말자.

    AndroidManifest.xml 파일의 가장 상위 엘리먼트인 <manifes> 바로 다음 자식 엘리먼트로 다음을 넣어주면 된다.

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

    이 부분에 대한 간략한 설명은 이전 포스트에도 살펴볼 수 있다.

    http://www.wiseant.net/blog/7 : android.webkit.WebView 를 이용한 데모

    자! 그러면 여기서 살펴보고자 하는 것은 안드로이드 애플리케이션을 SocketServer를 이용한 서버로 띄웠을 때의 이슈이다.

    그렇다면, 이에 해당하는 부분을 안드로이드 문서에서는 어떻게 정의해 두었을까? 다음의 링크를 통해서 확인할 수 있다.

    # emulator reference

    # emulator reference networking

    일종의 제약 사항이라고 할 수 있는데 에뮬레이터의 네트워크 아키텍쳐를 virtual router/firewall 을 사용하여 내부적으로 에뮬레이터내에 IP를 할당하고 있는 것을 볼 수 있다.
    이렇게되어 있다보니 로컬 시스템의 IP를 사용할 것으로 생각하면 오산인 것이다. 일단은 위의 문서를 토대로 대략적인 것을 살펴볼 수 있고, Network redirerction이란 것도 볼 수 있다.

    안드로이드 에뮬레이터에서 Network redirection은 에뮬레이터가 가지는 VLAN과 로컬 시스템의 포트를 연결(파이프로 연결한다고 생각하면 된다)해 주는 개념이다.
    즉, 로컬 시스템의 포트로 들어오는 패킷을 에뮬레이터의 포트로 넘겨주는 식이다(Forward 개념과 비슷하다).

    좀 복잡한 듯 하니 그냥 간단한 테스트 소스와 설정하는 방법을 통해서 알아보자.
    먼저, 안드로이드 애플리케이션에 올라가는 간단한 서버 소켓을 여는 프로그램을 만들어보자. 참고로 안드로이드 전체 소스는 생략한다. 많은 안드로이드 기초 강좌 등에서 찾아볼 수 있다.

        private String host = "localhost";
        private String port = 8002;
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle icicle) {
            super.onCreate(icicle);
            setContentView(R.layout.main);

            status = (TextView)findViewById(R.id.status);
            status.setText("do something");

            Button btnRunServer = (Button)findViewById(R.id.runserver);
            btnRunServer.setOnClickListener(new Button.OnClickListener() {
             public void onClick(View v) {
              // run server
              try {
               ServerSocket serverSocket = new ServerSocket(port);
               
               status.setText(status.getText() + " waiting");
               Log.d("Green", "Waiting...");
               
               Socket player1 = serverSocket.accept();

               status.setText(status.getText() + " Connected");
               Log.d("Green", "Connected");

               player1.close();
              }
              catch (Exception ex) {
               status.setText(status.getText() + ex.getMessage());
              }
             }
            });

            Button btnConnect = (Button)findViewById(R.id.connect);
            btnConnect.setOnClickListener(new Button.OnClickListener() {
             public void onClick(View v) {
              // connect to server
              try {
               Socket socket = new Socket(host, port);

               DataInputStream fromServer = new DataInputStream(socket.getInputStream());
               DataOutputStream toServer = new DataOutputStream(socket.getOutputStream());

               status.setText("connected");
               Log.d("Green", "Connected...");
               
               toServer.writeInt(1337);
              }
              catch (Exception ex) {
               status.setText(ex.getMessage());
              }
             }
            });
        }

    위의 소스에서는 ServerSocket을 열어놓고(Listening), accept 시에 Connected 메시만을 뿌려주는 간단한 서버용 애플리케이션이다.
    connect 버튼을 클릭 시에는 지정된 host로 접속해 보는 소스이다.

    안드로이드 에뮬레이터에서 접속 문제를 다루고 있음으로 여기선 ServerSocket 등에 대해서는 자세하게 다루지 않는다.

    일단 위의 소스를 기반으로 하여 안드로이드를 구동해 보자.

    일단 위와 같이 애플리케이션이 구동되면 첫 번째 버튼을 클릭해서 ServerSocket을 띄워준다. 이제 안드로이드 서버 애플리케이션과 연동할 간단한 클라이언트 소스를 살펴보자.

    /**
     * 
     */
    package com.mediachorus.test.android.network;

    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.net.Socket;

    /**
     * @author Sang-Hyup Lee
     * @version 1.0
     *
     */
    public class ClientTest {

     /**
      * @param args
      */
     public static void main(String[] args) {
      String host = "localhost";
      int port = 8002;
      // TODO Auto-generated method stub
      try {
       Socket socket = new Socket(host, port);

       DataInputStream fromServer = new DataInputStream(socket.getInputStream());
       DataOutputStream toServer = new DataOutputStream(socket.getOutputStream());

       System.out.println("IP:" + socket.getInetAddress().getHostAddress()
           + ", port:" + socket.getPort());
       System.out.println("Connected...");
          
       toServer.writeInt(1337);
       // toServer.write("quit".getBytes());
       toServer.flush();
       
       System.out.println("Done...");
      }
      catch (Exception ex) {
       System.err.println(ex.getMessage());
      }
     }
    }

    클라이언트 소스 역시 설명할 것도 별로 없다. 자바 네트워크 프로그램으로 서버에 접속해서 패킷을 보내어 본다. 로그를 통해서 접속이 되었는지 확인만 해 보는 것이다.

    클라이언트 소스를 실행해 보자. 다음과 같은 Exception을 발생시킨다.

    Connection refused: connect

    커넥션이 이루어지지 않았다는 말인데, 우리의 의도는 안드로이드 애플리케이션 TCP 서버 소켓에 접속하는 것이다. 동일한 PC 에서 이루어지는데 localhost 라고 하면 되는 줄 알았는데 아니다.

    여기서 위의 구글 문서 링크에서 나온 것처럼 Network redirection이 필요하다.
    다음과 같이 command 상에서 telnet 접속부터 시작해서 실행해보자. 참고로 에뮬레이터는 5554 포트를 기본으로 할당하여 telnet으로 접속이 가능하다.

    telnet localhost 5554
    redir add tcp:8002:8002 

    사용자 삽입 이미지

    위와 같이 로컬 시스템이 8002 포트를 사용하지 않으면 OK 메시지로 응답한다. 참고로 redir 사용법을 보려면 "redir help"라고 입력하면 된다.
    위의 명령어 뜻은 다음과 같다.

    TCP 형태로 redirection을 하는데, 로컬 시스템의 8002 포트를 VLAN(에뮬레이터에 올라간 안드로이드 애플리케이션) 8002 포트로 연결(redirection) 해 준다.

    add <protocol>:<host-port>:<guest-port>

    그렇다면 redirection을 해지하는 방법은 add를 del로 바꾸어주면 된다. 다음과 같은 형태이다.

    redir del tcp:8002


    del 옵션을 사용시에는 system host port만 입력해 주면 된다.

    자, 이 상태에서(redirection을 걸어둔 상태에서) 클라이언트 프로그램을 실행해 보자. Connected 메시지를 볼 수 있을 것이다.

    이렇게해서 기본적인 Network redirection을 해결할 수 있는데, 클라이언트 소스 코드를 '127.0.0.1'로 host 값을 바뀌어서 해 보자.
    (결론은? 잘 된다. 어쩌면 당연한 것을?^^)

    하지만, 로컬 시스템이 가지고 있는 IP를 host 값으로 수정하고 실행해 보자. 다시금 "Connection refused: connect" 에러가 발생한다.
    그렇다면, 안드로이드 애플리케이션이 실행하고 있는 시스템이 아닌 다른 시스템에서의 접속은? 당연히 안 된다.

    문제는 여기에 또 있는 것이다. Server/Client 프로그램은 당연히 서로 다른 시스템의 연결을 지양하고 있는 것인데 이런 문제가 발생한다.
    이는 안드로이드 에뮬레이터가 동작시에 localhost에 해당하는 127.0.0.1을 개발 머신의 loopback interface로 alias를 해주기 때문이다.
    결국은 로컬 시스템의 IP를 할당하여 수행하지 못한다는 것이다.

    이를 해결하기 위해서 다음의 링크를 통해서 stcppipe.exe를 실행해 주어야 한다. 링크에는 sudppipe.exe도 포함되어 있다. 소스와 함께.

    Simple UDP proxy/pipe 0.3a  : http://aluigi.org/mytoolz/sudppipe.zip
    Simple TCP proxy/pipe 0.4.4a : http://aluigi.org/mytoolz/stcppipe.zip

    사용법은 다음과 같다.

    stcppipe.exe -b <local IP> <remote hosts> <local port> <remote port>
    remote hosts : 127.0.0.1
    remote port : emulator 의 host port

    이를 응용해서 실행해 보면 다음과 같이 입력할 수 있다.

    stcppipe -b 192.168.0.86 l27.0.0.1 8002 8002

    이렇게 하고 다시금 로컬에서든 다른 시스템에서든지 Client 프로그램을 실행해보자.
    stcppipe 프로그램을 통해서 IN/OUT 되는 포트도 확인해 볼 수 있다. UDP도 마찬가지이다. 각자 테스트해 보는 것도 좋을 거 같다.


    이렇게해서 안드로이드 에뮬레이터의 Network redirection 이슈를 마친다. 끝으로 이 문제는 혼자서 고민하고 해결한 것이 아니라 kandroid.org 의 운영자이신 들풀님의 큰 도움이 있었다. 이에 감사의 뜻을 전하며 해당 부분을 Q&A를 통해서 해결하였는데, 링크를 걸어둔다.


    # 처음 Emulator IP 관련된 질문후, redir (option)에 대해서 알게된 링크

    # 로컬 시스템 내에서의 접속이 아닌 원격 시스템에서의 접속 처리를 해결하게 된 링크

    두 번째 링크에도 포함된 링크인데 구글 그룹스에서도 똑같은 고민을 했었고 동일하게 해결한 것을 찾을 수 있다.



    다시금 함께 고민해 주신 들풀님에게 감사의 뜻을 전한다.


    [마치며]
    지금까지의 Network redirection을 통해서 다양한 것을 개발해 보았다. 하지만 실제 개발에서 사용하는 것이 TCP, UDP를 함께 사용하면서 까다로운 네트워크 프로그램을 처리하는 것이었다. 특이한 점은 윈도우에서는 정상적으로 처리되지 않을 때가 많았다.
    그래서 Ubuntu 에서 실행해 보니 접속에 대한 문제는 거의 발생하지 않았다. 이러한 원인에 대한 분석은 현재로서는 어려울 것 같아서 다루지 않겠다.
    그냥 경험담이라고 생각해 주면 좋겠다.

    모두들... 더 좋은 정보나 자료가 있으면 공유해 주세요~^^ 
    Posted by 삼스
    Android/App개발2011.09.03 18:33


    Locale을 생성 후 updateConfigurations로 변경한다.
     

    Locale locale = new Locale("ko"); 

    Locale.setDefault(locale);

    Configuration config = new Configuration();

    config.locale = locale;

    getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());


    변경 후 액티비티를 다시 시작하여야 변경된 로켈이 적용된다.
     
    finish();

    Intent myIntent = new Intent(MainActivity.this, MainActivity.class);

    startActivity(myIntent);


    이렇게 하면 해당 액티비티에만 적용된다.
    액티비티를 시작할 때 이전에 선택한 로켈로 시작되게 하려면 onCreate에서 지정해야 한다.

    onCreate(..) {
             Locale locale2 = new Locale("en"); 

        Locale.setDefault(locale2);

            Configuration config2 = new Configuration();

            config2.locale = locale2;

            getBaseContext().getResources().updateConfiguration(config2, getBaseContext().getResources().getDisplayMetrics());


            setContentView(R.layout.main);

    }

    setContentView(.)보다 이전에 설정해주어야 한다.

    만일 이렇게만 하면 런타임에 로켈을 바꾸면 화면이 점점 더 작아지는 황당한 상황을 겪을 것이다.
    Manifest에 다음을 추가해라.

    <supports-screens

    android:smallScreens="true"

    android:normalScreens="true"

    android:largeScreens="true"

    android:anyDensity="true"

    />




     
    Posted by 삼스
    Android/App개발2011.06.16 18:31

    안드로이드 개발시 유닛테스트를 하는 방법에 대해 정리한다.

    두가지 케이스정도로 정리해보면
     1. 액티비티 테스트 : UI 테스트 자동화
     2. 클래스 테스트 : 로직을 담고 있는 클래스들을 테스트 자동화

    방법은 Android project를 만들 때 Android Test Project로 만든다. 이 때 테스트하고자 하는 앱을 지정하게 되어 있다. 
     
    1. 액티비티 테스트 
     -  ActivityInstrumentationTestCase2를 확장
    public class HelloActivityTest extends ActivityInstrumentationTestCase2<HelloActivity> 
     - 일부 메소드 override
    protected void setUp()
    protected void tearDown()
     - test 메소드들 구현
     - Activity에 접근은 this.getActivity()로 가능하다. 

    2. 클래스 테스트
     - InstrumentationTestCase 를 확장
     
     public class MetadataManagerTest extends InstrumentationTestCase 
     - 일부 메소드 override
    protected void setUp()
    protected void tearDown()
     - test 메소드들 구현
     
    AsyncTask를 테스트할 때는 CountdownLatch를 이용해서 비동기 스레드를 테스트 할 수 있다.
    출처 : http://kingori.egloos.com/4554640
    public class AsyncTaskTest extends InstrumentationTestCase {

      public void testAsyncTask() {
        final Data data = new Data();
        final CountDownLatch signal = new CountDownLatch(1); // countdown 1을 기다리는 랫치

        Callback a = new Callback() {
          onPrepare() { data.val = 1; }
          onSuccess() { data.val = 2; signal.countDown();} // 작업이 끝나면 1을 내려 유닛테스트 쓰레드가 진행되게 한다.
          onFail() { data.val = 3; signal.countDown(); } // 실패할 수도 있으니..
        }

        runTestOnUiThread( new Runnable() {
          public void run() {
            Manager.getInstance().doSomething(a);
          }
        });
        signal.await(); // 랫치 값이 0이 될 때 까지 대기 상태
        assertEquals( data.val, 2 ); //결과 확인
      }
    }

    Posted by 삼스