Mac에서 안드로이드 SDK의 Path 설정

|

Mac에서 터미널을 통해 adb 명령어를 실행하고 싶을 때가 종종 있습니다. Windows에서는 Android SDK가 설치된 경로를 환경변수로 등록해놓으면 되는데, Mac에서도 방법은 비슷합니다. 다만, Mac에서 Android SDK가 설치되어 있는 폴더 위치를 몰라서 Path 등록을 못하는 경우가 종종 있습니다.


Mac에서 Android SDK가 설치되는 폴더

사용자가 임의로 폴더 위치를 수정하지 않았다면, 기본적으로 Android SDK는 다음 위치에 설치됩니다.

/Users/snowdeer/Library/Android


.bash_profile 수정

환경 변수를 등록하기 위해서 .bash_profile을 수정하면 됩니다. 터미널에서 nano를 이용해서 수정을 해도 되고, 편하게 GUI 상의 텍스트편집기를 이용해서 수정을 해도 됩니다. 기본적으로 .bash_profile은 속성이 ‘숨김(Hidden)’으로 되어있습니다. Finder에서 숨김 파일을 보기 위해서는 Finder 창 안에서 단축키 shift + command + . 을 누르면 됩니다. .bash_profile/User/[사용자계정] 아래에 위치하고 있습니다.

그리고 .bash_profile 내에 다음과 같은 라인을 추가합니다.

export ANDROID_PATH=/Users/snowdeer/Library/Android
export PATH=$PATH:$ANDROID_PATH/sdk/platform-tools

이제 터미널을 다시 시작하고 adb 명령어를 입력해보면 잘 실행되는 것을 확인할 수 있습니다.

Map, Set 등 배열 내부 값 순회하기

|

프로그래밍을 하다보면, 리스트를 쓰는 경우는 피할 수가 없을 것입니다. 가장 많이 쓰는 것들이 일반적인 배열이나 ArrayList 들입니다. 그리고 성능이나 여러가지 장점을 위해 HashMap 등을 사용하게 되는 경우도 많은데 일반적인 배열들과 사용법이 조금 달라 혼란스러운 경우가 종종 있습니다.

물론, 구글링을 하면 바로 나오긴 하지만 나중에 좀 더 찾기 쉽게 하기 위해 여기다가 예제 코드를 작성해봅니다.


Iterator 사용

가장 기본적인 건 Iterator를 사용하는 것입니다. Iterator는 디자인 패턴 중 하나이기도 하고, 거의 모든 리스트에 적용가능할 정도로 범용적입니다. 하지만, 보통 개발자들은 Iterator 없이 단순 인덱스만 이용해서 for 문이나 while 반복문으로 접근하는 경향이 있습니다. 둘 다 문제없는 사용법이긴 하지만, 여기서는 먼저 Iterator를 이용한 접근 방법부터 살펴보도록 하겠습니다.

Map<String, String> map = new HashMap<>();

map.put("Key1", "Value1");
map.put("Key2", "Value2");
map.put("Key3", "Value3");

Iterator<String> iter = map.keySet().iterator();

System.out.println("#1. Using Iterator");
while(iter.hasNext()) {
    String key = iter.next();
    System.out.println(" -- " + key + ", " + map.get(key));
}


Set<String> set = new HashSet<>();

set.add("Value4");
set.add("Value5");
set.add("Value6");

Iterator<String> iter = set.iterator();

System.out.println("#1. Using Iterator");
while(iter.hasNext()) {
    String value = iter.next();
    System.out.println(" -- " + value);
}


for 문 사용

for 문을 사용하더라도 간단하게 조회할 수 있습니다. 다음 예제는 각각 Map과 Set에 대한 for 문으로의 접근 방법입니다.

System.out.println("#2. Using For");
for(String key : map.keySet()) {
    System.out.println(" -- " + key + ", " + map.get(key));
}


System.out.println("#2. Using For");
for(String value : set) {
    System.out.println(" -- " + value);
}

Notification 정보 가져오기

|

안드로이드 노티피케이션(Notification)의 정보를 가져오는 코드를 작성해보도록 하겠습니다. 안드로이드에서 NotificationListenerService라는 서비스 형태의 컴포넌트를 제공하고 있습니다. API 버전 18부터 사용가능하며, 원할하게 쓰려면 API 버전 19 이상을 추천합니다.

안드로이드 SDK에 기본으로 탑재되어 있기 때문에 gradle 등에 별도로 추가할 필요는 없고, manifest.xml에 다음과 같이 서비스를 등록해주면 됩니다.


AndroidManifest.xml

<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">
  ...
  <service
    android:name=".SnowNotificationListenerService"
    android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
    <intent-filter>
      <action android:name="android.service.notification.NotificationListenerService"/>
    </intent-filter>
  </service>
</application>


그리고, Java 코드는 다음과 같습니다.

SnowNotificationListenerService.java

import android.app.Notification;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;

public class SnowNotificationListenerService extends NotificationListenerService {

  @Override
  public void onCreate() {
    super.onCreate();
    Log.i("NotificationListener", "[snowdeer] onCreate()");
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    Log.i("NotificationListener", "[snowdeer] onStartCommand()");
    return super.onStartCommand(intent, flags, startId);
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    Log.i("NotificationListener", "[snowdeer] onDestroy()");
  }

  @Override
  public void onNotificationPosted(StatusBarNotification sbn) {
    Log.i("NotificationListener", "[snowdeer] onNotificationPosted() - " + sbn.toString());
    Log.i("NotificationListener", "[snowdeer] PackageName:" + sbn.getPackageName());
    Log.i("NotificationListener", "[snowdeer] PostTime:" + sbn.getPostTime());

    Notification notificatin = sbn.getNotification();
    Bundle extras = notificatin.extras;
    String title = extras.getString(Notification.EXTRA_TITLE);
    int smallIconRes = extras.getInt(Notification.EXTRA_SMALL_ICON);
    Bitmap largeIcon = ((Bitmap) extras.getParcelable(Notification.EXTRA_LARGE_ICON));
    CharSequence text = extras.getCharSequence(Notification.EXTRA_TEXT);
    CharSequence subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT);

    Log.i("NotificationListener", "[snowdeer] Title:" + title);
    Log.i("NotificationListener", "[snowdeer] Text:" + text);
    Log.i("NotificationListener", "[snowdeer] Sub Text:" + subText);
  }

  @Override
  public void onNotificationRemoved(StatusBarNotification sbn) {
    Log.i("NotificationListener", "[snowdeer] onNotificationRemoved() - " + sbn.toString());
  }

}


참고로 이 서비스는 별도로 startService를 해주지 않아도, 사용자의 권한만 주어지면 자동으로 시작되기 때문에 startService를 구현할 필요는 없습니다.

사용자의 권한을 요청하는 코드는 다음과 같습니다.

Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
startActivity(intent);


하지만, 위 코드를 매번 호출하는 것은 사용자에게 상당히 번거로운 일이기 때문에, 다음과 같은 코드를 이용해서 기존에 사용자가 해당 App에 권한을 부여한 적이 있는지 확인하는 것이 좋습니다.


MainActivity.java

import android.content.Intent;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import java.util.Set;

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    boolean isPermissionAllowed = isNotiPermissionAllowed();

    if(!isPermissionAllowed) {
      Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
      startActivity(intent);
    }
  }

  private boolean isNotiPermissionAllowed() {
    Set<String> notiListenerSet = NotificationManagerCompat.getEnabledListenerPackages(this);
    String myPackageName = getPackageName();

    for(String packageName : notiListenerSet) {
      if(packageName == null) {
        continue;
      }
      if(packageName.equals(myPackageName)) {
        return true;
      }
    }

    return false;
  }
}

Butter Knife 라이브러리 사용법

|

안드로이드 GUI 개발을 하다보면, 가장 귀찮은 것 중 하나가 화면에 컴포넌트를 하나 추가하는 것입니다.

예를 들어 화면에 버튼을 추가한다고 하면 다음과 같은 작업들을 거쳐야 합니다.

  • XML Layout에 버튼을 추가한다.
  • 해당 Activity(Fragment)에서 그 컴포넌트에 해당하는 변수를 추가한다. ex) private Button button;
  • findViewById를 통해 그 컴포넌트를 변수에 할당한다. ex) button = (Button)findViewById(R.id.button)
  • 그 버튼에 setOnClickListener를 통해 이벤트를 등록한다.

컴포넌트가 몇 개 안 될 경우는 큰 문제가 안되는데, 컴포넌트가 많아질수록 위 코드들은 감당이 안 될 정도로 많아지고 복잡해집니다. 이럴 때 ‘Butter Knife’ 라이브러리를 사용하면 코드량이 상당히 줄어듭니다. (사실, 이런 식으로 별거 아닌 코드를 Wrapping 하는 외부 라이브러리를 쓰는 걸 선호하진 않지만 Butter Knife는 생각보다 괜찮은 것 같아서 최근에 조금씩 사용을 해보고 있습니다.)

간단한 예제 코드로 알아보도록 하겠습니다. 더 자세한 내용은 공식 홈페이지를 참조하시면 됩니다. 버전에 따라 문법이 조금씩 바뀌고 있는데, 여기서는 현재 최신 버전인 8.5.1 기준으로 사용해보겠습니다.


gradle 세팅

dependencies {
    ...
    compile 'com.jakewharton:butterknife:8.5.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
    ...
}


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/activity_main"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  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:orientation="vertical"
  tools:context="com.snowdeer.butterknife.sample.MainActivity">

  <TextView
    android:id="@+id/title"
    android:textStyle="bold"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Title"
    android:textSize="32sp" />

  <Button
    android:id="@+id/button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Click Me!" />

  <ImageView
    android:id="@+id/imageview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

</LinearLayout>


strings.xml

<resources>
  <string name="app_name">ButterKnifeSample</string>
  <string name="title">Hello. Butter Knife</string>
</resources>


MainActivity.java

여기서 가장 중요한 부분은 onCreate() 함수 내의 다음 코드입니다.

ButterKnife.bind(this);

위 코드를 호출해주어야 각 View들이 바인딩(Binding)이 됩니다.


import android.graphics.drawable.Drawable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.TextView;
import butterknife.BindDrawable;
import butterknife.BindString;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity {

  @BindView(R.id.title)
  TextView titleView;
  @BindView(R.id.imageview)
  ImageView imageView;
  @BindString(R.string.title)
  String title;
  @BindDrawable(R.mipmap.ic_launcher)
  Drawable drawable;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ButterKnife.bind(this);
  }

  @OnClick(R.id.button)
  void onButtonClicked() {
    titleView.setText(title);
    imageView.setImageDrawable(drawable);
  }

  ;
}


Fragment 에서의 예제

public class FancyFragment extends Fragment {

  @BindView(R.id.button1)
  Button button1;
  @BindView(R.id.button2)
  Button button2;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fancy_fragment, container, false);
    ButterKnife.bind(this, view);
    // TODO Use fields...
    return view;
  }
}


ListView 등에서 사용하는 Adapter 및 ViewHolder 예제

public class MyAdapter extends BaseAdapter {

  @Override
  public View getView(int position, View view, ViewGroup parent) {
    ViewHolder holder;
    if(view != null) {
      holder = (ViewHolder) view.getTag();
    } else {
      view = inflater.inflate(R.layout.whatever, parent, false);
      holder = new ViewHolder(view);
      view.setTag(holder);
    }

    holder.name.setText("John Doe");
    // etc...

    return view;
  }

  static class ViewHolder {

    @BindView(R.id.title)
    TextView name;
    @BindView(R.id.job_title)
    TextView jobTitle;

    public ViewHolder(View view) {
      ButterKnife.bind(this, view);
    }
  }
}


이벤트 등록 예제

@OnClick(R.id.submit)
public void submit(View view) {
  // TODO submit data to server...
}

@OnClick(R.id.submit)
public void submit() {
  // TODO submit data to server...
}

@OnClick(R.id.submit)
public void sayHi(Button button) {
  button.setText("Hello!");
}

@OnClick({R.id.door1, R.id.door2, R.id.door3})
public void pickDoor(DoorView door) {
  if(door.hasPrizeBehind()) {
    Toast.makeText(this, "You win!", LENGTH_SHORT).show();
  } else {
    Toast.makeText(this, "Try again", LENGTH_SHORT).show();
  }
}

Assets의 텍스트 파일 불러오기

|

Assets 폴더에 들어있는 텍스트 파일의 내용을 String으로 읽는 코드입니다.

Android Studio 기준으로 Assets 폴더는 /app/src/main/assets에 위치하며, 네비게이션 뷰를 Project로 설정한 다음 폴더를 추가하면 됩니다.

본 예제는 텍스트 파일을 읽어오는 코드이지만, 다른 파일들에도 적용할 수 있습니다.

예제 코드는 다음과 같습니다.


PolicyActivity.java

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Html;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.lnc.cuppadata.R;
import com.lnc.cuppadata.gui.account.RegistrationActivity;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class PolicyActivity extends AppCompatActivity {

  @BindView(R.id.policy_view)
  protected TextView policyView;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_policy);

    ButterKnife.bind(this);

    loadPolicy();
  }

  @OnClick(R.id.ok)
  protected void onOkButtonPressed() {
    Intent intent = new Intent(PolicyActivity.this, RegistrationActivity.class);
    startActivity(intent);

    finish();
  }

  @OnClick(R.id.cancel)
  protected void onCancelButtonPressed() {
    finish();
  }

  private void loadPolicy() {
    try {
      String policy = readFromAssets("policy.txt");

      policyView.setText(Html.fromHtml(policy));
    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  private String readFromAssets(String filename) throws Exception {
    BufferedReader reader = new BufferedReader(new InputStreamReader(getAssets().open(filename)));

    StringBuilder sb = new StringBuilder();
    String line = reader.readLine();
    while(line != null) {
      sb.append(line);
      line = reader.readLine();
    }
    reader.close();
    return sb.toString();
  }
}