블루투스 기반 소켓 통신 예제

|

안드로이드에서 블루투스(Bluetooth)를 이용하여 간단하게 단말간 통신할 수 있는 코드를 포스팅해봅니다.

단말 2개가 필요하며, 한 쪽 단말은 서버, 한 쪽 단말은 클라이언트가 됩니다. 또한, 두 단말간의 페어링(Pairing) 단계는 이미 되었다고 가정하고 건너뛰도록 하겠습니다.


권한(Permission) 추가

블루투스 통신을 사용하기 위해서는 아래 두 권한이 필요합니다. AndroidManifest.xml 파일에 추가해주시면 됩니다.

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


상수값 설정

public class SnowConstant {
  public static final UUID BLUETOOTH_UUID_SECURE = UUID.fromString( "fa87c0d0-afac-11de-8a39-0800200c9a66" );
  public static final UUID BLUETOOTH_UUID_INSECURE = UUID.fromString( "8ce255c0-200a-11e0-ac64-0800200c9a66" );
}


서버

fragment_btserver.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <Button
    android:id="@+id/start_server"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Start Server" />

  <Button
    android:id="@+id/stop_server"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Stop Server" />

  <Button
    android:id="@+id/send_welcome"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Send Welcome Message" />

  <TextView
    android:id="@+id/logview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

</LinearLayout>


BTServerFragment.java

public class BTServerFragment extends Fragment {

  Handler handler = new Handler();
  TextView logView;
  StringBuilder sbLog = new StringBuilder();

  BluetoothServer btServer = new BluetoothServer();

  public BTServerFragment() {
    // Required empty public constructor
  }


  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View view = inflater.inflate(R.layout.fragment_btserver, container, false);

    logView = (TextView) view.findViewById(R.id.logview);
    btServer.setOnEventLogListener(new OnEventLogListener() {
      @Override
      public void onLog(String message) {
        log(message);
      }
    });

    view.findViewById(R.id.start_server).setOnClickListener(mOnClickListener);
    view.findViewById(R.id.stop_server).setOnClickListener(mOnClickListener);
    view.findViewById(R.id.send_welcome).setOnClickListener(mOnClickListener);

    return view;
  }

  private View.OnClickListener mOnClickListener = new View.OnClickListener() {

    @Override
    public void onClick(View v) {
      switch (v.getId()) {
        case R.id.start_server:
          btServer.startServer();
          break;
        case R.id.stop_server:
          btServer.stopServer();
          break;
        case R.id.send_welcome:
          btServer.sendWelcome();
          break;
      }
    }
  };

  void log(String message) {
    sbLog.append(message + "\n");

    if (logView != null) {
      handler.post(new Runnable() {
        @Override
        public void run() {
          logView.setText(sbLog.toString());
        }
      });
    }

    Log.i("", "[snowdeer] " + message);
  }
}


BluetoothServer.java

public class BluetoothServer {

  BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
  BluetoothServerSocket acceptSocket;

  AcceptThread acceptThread;

  DataInputStream is = null;
  DataOutputStream os = null;

  public interface OnEventLogListener {

    void onLog(String message);
  }

  OnEventLogListener mOnEventLogListener = null;

  public void setOnEventLogListener(OnEventLogListener listener) {
    mOnEventLogListener = listener;
  }

  void printLog(String message) {
    if (mOnEventLogListener != null) {
      mOnEventLogListener.onLog(message);
    }
  }

  public void startServer() {
    stopServer();

    printLog("Start Server.");
    acceptThread = new AcceptThread();
    acceptThread.start();
  }

  public void stopServer() {
    if (acceptThread == null) {
      return;
    }

    try {
      acceptThread.stopThread();
      acceptThread.join(1000);
      acceptThread.interrupt();
    } catch (Exception e) {
      e.printStackTrace();
    }

    printLog("Stop Server.");
  }

  class AcceptThread extends Thread {

    boolean isRunning = false;

    AcceptThread() {
      try {
        acceptSocket = null;
        acceptSocket = btAdapter.listenUsingRfcommWithServiceRecord("SnowDeerBluetoothSample",
            SnowConstant.BLUETOOTH_UUID_INSECURE);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

    public void run() {
      BluetoothSocket socket;

      isRunning = true;

      while (isRunning) {
        try {
          printLog("Waiting clients...");
          socket = acceptSocket.accept();
        } catch (Exception e) {
          e.printStackTrace();
          break;
        }

        if (socket != null) {
          printLog("Client is connected.");

          CommunicationThread commThread = new CommunicationThread(socket);
          commThread.start();
        }
      }
    }

    public void stopThread() {
      isRunning = false;
      try {
        acceptSocket.close();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  class CommunicationThread extends Thread {

    final BluetoothSocket socket;
    boolean isRunning = false;

    CommunicationThread(BluetoothSocket socket) {
      this.socket = socket;
    }

    public void run() {
      isRunning = true;

      try {
        is = new DataInputStream(socket.getInputStream());
        os = new DataOutputStream(socket.getOutputStream());
      } catch (Exception e) {
        e.printStackTrace();
      }

      while (isRunning) {
        try {
          String message = is.readUTF();
          printLog(message);
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }

    public void stopThread() {
      isRunning = false;
      try {
        is.close();
        os.close();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  public void sendWelcome() {
    if (os == null) {
      return;
    }

    try {
      os.writeUTF("Welcome");
      os.flush();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}


클라이언트

fragment_btclient.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <Button
    android:id="@+id/search_devices"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Search Devices" />

  <Button
    android:id="@+id/connect_to_server"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Connect to Server" />

  <Button
    android:id="@+id/disconnect_from_server"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Disconnect from Server" />

  <Button
    android:id="@+id/send_hello"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Send Hello Message" />

  <TextView
    android:id="@+id/logview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

</LinearLayout>


BTClientFragment.java

public class BTClientFragment extends Fragment {

  Handler handler = new Handler();
  TextView logView;
  StringBuilder sbLog = new StringBuilder();

  BluetoothClient btClient = new BluetoothClient();

  public BTClientFragment() {
    // Required empty public constructor
  }


  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {
    View view =  inflater.inflate(R.layout.fragment_btclient, container, false);

    logView = (TextView) view.findViewById(R.id.logview);
    btClient.setOnEventLogListener(new OnEventLogListener() {
      @Override
      public void onLog(String message) {
        log(message);
      }
    });

    view.findViewById(R.id.search_devices).setOnClickListener(mOnClickListener);
    view.findViewById(R.id.connect_to_server).setOnClickListener(mOnClickListener);
    view.findViewById(R.id.disconnect_from_server).setOnClickListener(mOnClickListener);
    view.findViewById(R.id.send_hello).setOnClickListener(mOnClickListener);

    return view;
  }

  private View.OnClickListener mOnClickListener = new View.OnClickListener() {

    @Override
    public void onClick(View v) {
      switch (v.getId()) {
        case R.id.search_devices:
          btClient.searchPairedDevice();
          break;
        case R.id.connect_to_server:
          btClient.connectToServer();
          break;
        case R.id.disconnect_from_server:
          btClient.disconnectFromServer();
          break;
        case R.id.send_hello:
          btClient.sendHello();
          break;
      }
    }
  };

  void log(String message) {
    sbLog.append(message + "\n");

    if (logView != null) {
      handler.post(new Runnable() {
        @Override
        public void run() {
          logView.setText(sbLog.toString());
        }
      });
    }

    Log.i("", "[snowdeer] " + message);
  }
}


BluetoothClient.java

public class BluetoothClient {

  BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
  BluetoothDevice mTargetDevice = null;

  DataInputStream is = null;
  DataOutputStream os = null;

  ClientThread clientThread;

  public interface OnEventLogListener {

    void onLog(String message);
  }

  OnEventLogListener mOnEventLogListener = null;

  public void setOnEventLogListener(OnEventLogListener listener) {
    mOnEventLogListener = listener;
  }

  void printLog(String message) {
    if (mOnEventLogListener != null) {
      mOnEventLogListener.onLog(message);
    }
  }

  public void searchPairedDevice() {
    Set<BluetoothDevice> pairedDevices = btAdapter.getBondedDevices();
    printLog("paired Devices count : " + pairedDevices.size());

    if (pairedDevices.size() > 0) {
      for (BluetoothDevice deivce : pairedDevices) {
        printLog(deivce.getName() + " / " + deivce.getAddress());

        mTargetDevice = deivce;

        printLog(deivce.getName() + "(" + deivce.getAddress() + ") is found !!!");
      }
    }
  }

  public void connectToServer() {
    disconnectFromServer();

    if (mTargetDevice == null) {
      return;
    }

    printLog("Connect to Server.");
    clientThread = new ClientThread(mTargetDevice);
    clientThread.start();
  }

  public void disconnectFromServer() {
    if (clientThread == null) {
      return;
    }

    try {
      clientThread.stopThread();
      clientThread.join(1000);
      clientThread.interrupt();
    } catch (Exception e) {
      e.printStackTrace();
    }

    printLog("Disconnect from Server.");
  }


  class ClientThread extends Thread {

    BluetoothSocket socket;
    BluetoothDevice mDevice;

    ClientThread(BluetoothDevice device) {
      try {
        mDevice = device;

        //socket = null;

        socket = device.createRfcommSocketToServiceRecord(SnowConstant.BLUETOOTH_UUID_INSECURE);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

    public void run() {
      btAdapter.cancelDiscovery();

      while (true) {
        try {
          printLog("try to connect to Server...");
          socket.connect();
        } catch (Exception e) {
          e.printStackTrace();
          try {
            socket.close();
          } catch (Exception ex) {
            ex.printStackTrace();
          }
          break;
        }

        if (socket != null) {
          printLog("Connected !!!");

          try {
            is = new DataInputStream(socket.getInputStream());
            os = new DataOutputStream(socket.getOutputStream());

          } catch (Exception e) {
            e.printStackTrace();
          }

          while (true) {
            try {
              String message = is.readUTF();
              printLog(message);
            } catch (Exception e) {
              e.printStackTrace();
            }
          }
        }

        try {
          sleep(1000);
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }

    public void stopThread() {
      try {
        socket.close();
        is.close();
        os.close();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  public void sendHello() {
    if (os == null) {
      return;
    }

    try {
      os.writeUTF("Hello");
      os.flush();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

안드로이드 Navigation Drawer 사용하기

|

요즘 나오는 App들은 Navigation Drawer를 사용한 UX가 많습니다.

image


Google Developer 사이트에서 훌륭한 예제 코드를 다루고 있는데, 여기서는 Android Studio에서 자동으로 생성해준 템플릿 코드 기반으로 간단하게 구현을 해보았습니다.

먼저 메뉴 항목들을 나열할 xml 파일을 만들어줍니다. (res 폴더 아래 menu 폴더 아래 만들어 주시면 됩니다.)


activity_main_drawer.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

  <group android:checkableBehavior="single">
    <item
      android:id="@+id/menu_bt_server"
      android:icon="@drawable/ic_menu_camera"
      android:title="Bluetooth Server" />
    <item
      android:id="@+id/menu_bt_client"
      android:icon="@drawable/ic_menu_gallery"
      android:title="Bluetooth Client" />

  </group>

</menu>


그리고, 액티비티에서 사용할 레이아웃은 다음과 같이 작성합니다.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/drawer_layout"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:fitsSystemWindows="true"
  tools:openDrawer="start">

  <include
    layout="@layout/app_bar_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

  <android.support.design.widget.NavigationView
    android:id="@+id/nav_view"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:fitsSystemWindows="true"
    app:headerLayout="@layout/nav_header_main"
    app:menu="@menu/activity_main_drawer" />

</android.support.v4.widget.DrawerLayout>


app_bar_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context="snowdeer.bluetooth.sample.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay" />

  </android.support.design.widget.AppBarLayout>

  <RelativeLayout
    android:id="@+id/content_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>


nav_header_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="@dimen/nav_header_height"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:background="@drawable/side_nav_bar"
  android:gravity="bottom"
  android:orientation="vertical"
  android:theme="@style/ThemeOverlay.AppCompat.Dark">

  <ImageView
    android:id="@+id/imageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/nav_header_vertical_spacing"
    app:srcCompat="@android:drawable/sym_def_app_icon" />

  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/nav_header_vertical_spacing"
    android:text="Android Studio"
    android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

  <TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="android.studio@android.com" />

</LinearLayout>


그리고 실제 Java 코드는 다음과 같이 작성하면 됩니다.

MainActivity.java

public class MainActivity extends AppCompatActivity
    implements NavigationView.OnNavigationItemSelectedListener {

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

    DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
    ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
        this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawer.setDrawerListener(toggle);
    toggle.syncState();

    NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
    navigationView.setNavigationItemSelectedListener(this);

    replaceServerFragment();
  }

  @Override
  public void onBackPressed() {
    DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
    if (drawer.isDrawerOpen(GravityCompat.START)) {
      drawer.closeDrawer(GravityCompat.START);
    } else {
      super.onBackPressed();
    }
  }

  @SuppressWarnings("StatementWithEmptyBody")
  @Override
  public boolean onNavigationItemSelected(MenuItem item) {
    int id = item.getItemId();

    if (id == R.id.menu_bt_server) {
      replaceServerFragment();
    } else if (id == R.id.menu_bt_client) {
      replaceClientFragment();
    }

    DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
    drawer.closeDrawer(GravityCompat.START);
    return true;
  }

  void replaceServerFragment() {
    setTitle("Bluetooth Server");
    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    Fragment fragment = new BTServerFragment();
    ft.replace(R.id.content_main, fragment);
    ft.commit();
  }

  void replaceClientFragment() {
    setTitle("Bluetooth Client");
    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    Fragment fragment = new BTClientFragment();
    ft.replace(R.id.content_main, fragment);
    ft.commit();
  }
}

안드로이드 상태바 투명 처리

|

안드로이드 상태바를 투명하게 만드는 방법입니다.

대충 안드로이드 상태바는 다음과 같은 형태로 표현할 수 있습니다.

1) 아무런 설정을 안 해줬을 때의 기본적인 상태바

image

2) 색상을 입힌 상태바

image

3) 투명 처리를 한 상태바

image

과거에는 완전히 투명한 상태바도 표현할 수 있었는데, 지금은 반투명 상태로 표현되고 있습니다.


일단, 반투명 상태의 상태바는 다음과 같은 테마를 적용하여 구현할 수 있습니다.


styles.xml

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
  <!-- Customize your theme here. -->
  <item name="colorPrimary">@color/colorPrimary</item>
  <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  <item name="colorAccent">@color/colorAccent</item>
  <item name="android:windowDrawsSystemBarBackgrounds">true</item>
  <item name="android:statusBarColor">@color/transparent</item>
  <item name="android:windowTranslucentStatus">true</item>

</style>


때에 따라서 반투명 상태보다는 색상을 입힌 상태바가 더 좋을 때가 있습니다. 상태바의 색상을 primaryColor와 똑같이 지정하면 투명 상태처럼 보이기도 합니다. 그럴 때는

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
  <!-- Customize your theme here. -->
  <item name="colorPrimary">@color/colorPrimary</item>
  <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  <item name="colorAccent">@color/colorAccent</item>
  <item name="android:windowDrawsSystemBarBackgrounds">true</item>
  <item name="android:statusBarColor">@color/colorPrimary</item>
  <item name="android:windowTranslucentStatus">false</item>

</style>

와 같이 지정해주면 됩니다.

7.0(Nougat) 에서 Dialog 들의 버튼들이 보이지 않는 현상

|

안드로이드 7.0 Nougat에서 다음 이미지와 같이 Dialog들의 버튼이 사라져 버리는 현상이 있습니다.

image

AlertDialog 뿐만 아니라 PickerDialog 들도 마찬가지 현상이 발생했습니다.

image

기존에 잘 되던 코드였는데, 갑자기 안드로이드 7.0을 올린 사람들에게 이런 반응이 나와서 찾아보니 7.0 부터는 Dialog에 테마(theme)를 적용해야 하는 정책이 생겼다고 합니다. 테마를 적용하지 않은 기존의 Dialog 들의 버튼은 투명 처리되어 보이지 않습는다. (투명 처리만 되어있어서 해당 버튼이 있는 위치를 누르면 동작은 한다고 합니다;;)  갑자기 왜 이런 하위 호환성을 무시해버린 정책을 만들었는지 모르겠지만, 일단은 해결법을 알아보도록 하겠습니다.


먼저 Dialog용 테마를 하나 만듭니다.

styles.xml

<style name="AlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert">
  <item name="colorPrimary">@color/colorPrimary</item>
  <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  <item name="colorAccent">@color/colorAccent</item>
  <item name="borderlessButtonStyle">@style/Widget.AppCompat.Button.Borderless.Colored</item>
</style>


그리고 기존에 AlertDialog를 작성하던 코드가 다음과 같았다면

기존 코드

mAlertDialog = new AlertDialog.Builder(getActivity())
    .setTitle("쿠폰 구입")
    .setMessage("'" + item.name + "'를 정말로 구매하시겠습니까? 포인트가 " + item.price + " 원 차감됩니다.")

다음과 같이 수정합니다.


수정된 코드

mAlertDialog = new AlertDialog.Builder(getActivity(), R.style.AlertDialogTheme)
    .setTitle("쿠폰 구입")
    .setMessage("'" + item.name + "'를 정말로 구매하시겠습니까? 포인트가 " + item.price + " 원 차감됩니다.")

현재 Application Version을 코드에서 사용하기

|

안드로이드 App을 개발할 때 App Version을 소스 코드내에서 활용하고 싶을 때가 있습니다.

Eclipse의 경우에는 App Version이 manifest.xml 파일 내에 정의되어 있는데, Android Studio에서는 build.gradle 내에 버전이 입력되어 있습니다.


build.gradle

App Version은 build.gradle 내에 다음과 같이 설정됩니다.

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    defaultConfig {
        multiDexEnabled true
        applicationId "com.lnc.cuppadata"
        minSdkVersion 21
        targetSdkVersion 25
        versionCode 10
        versionName "0.10"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    ...



그리고 안드로이드 소스내에서 App Version을 가져오는 코드는 다음과 같습니다.

소스 코드

private String getAppVersion() {
  try {
    PackageInfo pInfo = getActivity().getPackageManager().getPackageInfo(
        getActivity().getPackageName(), 0);

    if(pInfo != null) {
      return pInfo.versionName;
    }
  } catch(Exception e) {
    e.printStackTrace();
  }

  return "";
}