Base64 인코딩

|

Base64

Base64는 64진법이라는 뜻입니다. 화면에 표현하는 문자 종류가 64가지라는 뜻으로 이해하면 됩니다. 전자 메일이나 바이너리 데이터를 전송할 때 많이 사용됩니다. Base64의 정확한 규격은 RFC 1421, RFC 2045에 정의되어 있습니다.


사용하는 문자

Base64에서 사용하는 문자는 A-Z, a-z, 0-9의 총 62가지에 기호 2개로 이루어져 있습니다. 기호 2개는 보통 ‘+’와 ‘/’으로 구성되는데, Base64 변종들에 따라서 조금씩 다르기도 합니다. 그리고 끝을 알리는 코드로 ‘=’를 사용합니다.

image


한계점

Base64의 한계점은 보통 대문자, 소문자, 숫자의 62개 문자외에 추가로 사용하는 기호 2개에서 옵니다. 해당 문자는 파일 이름이나 URL 주소 등으로 사용할 수 없는 특수 문자입니다. 그러다보니 기호 2개를 뺀 62개의 문자로 표현하는 Base62나 눈으로 보면 헷갈리는 문자들(예를 들어 O와 o, 숫자 0 등)을 제외한 Base58 등을 사용하기도 합니다.

URL 주소를 이용하여 ImageView에 이미지 채우기 (Glide 활용)

|

URL 주소를 이용한 이미지 로딩

이미지의 URL 주소를 알고 있을 때, 그 이미지를 ImageView에 그리는 방법은 다음과 같습니다.

  • 이미지를 다운로드한다.
  • 다운로드한 이미지를 Bitmap 로딩하여 ImageView에 채워 넣는다.

이 경우, 구현해야 할 코드의 양도 꽤 많지만, 성능 문제를 위해 여러 가지 기법을 적용해야 합니다. 예를 들면, 다음과 같습니다.

  • ListView 등과 같이 여러 장의 이미지를 동시에 다운로드 할 수 있는 Thread 환경 구현
  • 한 번 다운로드 받은 이미지는 다시 받지 않도록, 그리고 성능을 위해 Image Cache 구현
  • Thread로 다운로드하는 동안 화면에는 ProgressBar를 표시


기본적으로 위의 요소들이 있지만, 하나 하나를 파고들면 고려해볼 것이 상당히 많습니다. 예를 들면 Image Cache에 무한대의 이미지를 저장할 수 없기 때문에 Cache의 리소스 관리도 필요하고, 이미지 다운로드 및 로딩이 끝났을 때 어느 ImageView에 이미지를 그릴 것인지 관리하는 ImageView의 레퍼런스 저장도 필요합니다. 또한 이러한 것들은 WeakReference로 관리하여 해당 ImageView가 메모리에 여전히 남아 있는지 GC(Garbage Collector)로 청소가 끝났는지도 체크해야 합니다.

예전에 프로젝트를 진행하면서 직접 구현한 적이 있었는데, 신경쓸게 너무 많아서 고생을 한 적이 있습니다. 물론, 그만큼 공부는 많이 되긴 했습니다.


Glide

이러한 것들을 한 꺼번에 해소해주기 위해서 오픈 소스 활용을 추천합니다. 오픈 소스는 유명한 것들이 많이 있는데, 5년 정도 전만 하더라도 저는 Android Universal Image Loader를 애용하였습니다. 지금도 무난히 사용하기에 충분히 괜찮은 라이브러리입니다. 하지만, 지금은 Glide 라이브러리를 사용해보려고 합니다. Glide는 구글이 인수한 Bump라는 회사에서 사용한 이미지 로딩 라이브러리입니다.


사용 방법

Gradle에 다음 코드를 추가해줍니다.

dependencies{
    compile'com.github.bumptech.glide:glide:3.7.0'
    ...
}


그리고 별도로 XML 코드를 건드릴 필요 없고, 다음과 같은 Java 코드로 ImageView에 이미지를 불러오게 할 수 있습니다.

Glide
    .with(mContext)
    .load(item.image)
    .centerCrop()
    //.placeholder(R.drawable.loading_spinner)
    .crossFade()
    .into(holder.image);

이렇게만 하면 끝입니다. 더 자세한 사용법은 여기에서 확인할 수 있습니다.

OkHttp를 활용한 GET, POST

|

OkHttp는 HTTP 및 HTTP/2 통신을 보다 쉽게 할 수 있도록 다양한 기능을 제공해주는 Android 및 Java 용 라이브러리입니다. 오픈 소스로 되어 있으며 GitHub에서 소스 확인 및 다운로드할 수 있습니다.


Gradle 설정

build.gradle에 다음 라인을 추가해줍니다.

compile 'com.squareup.okhttp3:okhttp:3.6.0'


GET 예제

public boolean getUserInfo(final Context context) {

  try {
    OkHttpClient client = new OkHttpClient();

    String url = SERVER_CONFIGURATION.ADDRESS + ":" +
        SERVER_CONFIGURATION.PORT + "/v1/userinfo";

    Request request = new Request.Builder()
        .addHeader("Authorization", "TEST AUTH")
        .url(url)
        .build();
    Response response = client.newCall(request)
        .execute();

    String result = response.body().string();

    Gson gson = new Gson();
    UserInfo info = gson.fromJson(result, UserInfo.class);

    Log.i("id: " + info.id);
    Log.i("name: " + info.name);

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

  return false;
}


POST 예제

private boolean updatetMetaInfo(JsonItemMetaInfo metaInfo) {

  try {
    OkHttpClient client = new OkHttpClient();

    String url = SERVER_CONFIGURATION.ADDRESS + ":" +
        SERVER_CONFIGURATION.PORT + "/v1/updateMetaInfo";

    Gson gson = new Gson();
    String json = gson.toJson(metaInfo);

    Request request = new Request.Builder()
        .url(url)
        .post(RequestBody.create(MediaType.parse("application/json"), json))
        .build();

    Response response = client.newCall(request).execute();

    Log.i("request : " + request.toString());
    Log.i("Response : " + response.toString());

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

  return false;
}


PUT 예제

public boolean registerAppToken(JsonToken token) {
  try {
    OkHttpClient client = new OkHttpClient();

    String url = SERVER_CONFIGURATION.ADDRESS + ":" +
        SERVER_CONFIGURATION.PORT + "/v1/registerAppToken";

    Gson gson = new Gson();
    String json = gson.toJson(token);

    Request request = new Request.Builder()
        .addHeader("key", "Content-Type")
        .addHeader("value", "application/json")
        .addHeader("description", "")
        .url(url)
        .put(RequestBody.create(MediaType.parse("application/json"), json))
        .build();

    Response response = client.newCall(request).execute();

    Log.i("request : " + request.toString());
    Log.i("Response : " + response.toString());

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

  return false;
}

프로그램 강제 종료시키기

|

프로그램 강제 종료시키기

MMac에서 어플리케이션들이 응답 없음 상태(바람개비가 빙글빙글 돌아가면서)가 되는 경우가 종종 있습니다. 이런 경우 ‘닫기’ 버튼도 먹통이 되고 종료 단축키인 Command + Q 도 먹통이 됩니다.

이 경우는 응용 프로그램 강제 종료 기능을 호출해서 해당 어플을 종료시킬 수 있습니다.

image


단축키

단축키는 다음과 같습니다.

Command + Option + ESC

ExpandableListView 예제

|

Android에서 다음 그림과 같이 Depth가 존재하는 ListView를 만들고 싶을 때는 어떻게 해야 할까요? 여러 가지 방법이 있긴 한데, 가장 간편하게 만들 수 있는 방법으로는 ExpandableListView를 사용하는 방법이 있습니다. 이 UI 컴포넌트는 Android SDK에 기본으로 포함되어 있습니다.

image

위와 같은 화면을 구현하려면 크게 다음과 같은 요소들을 구현해주면 됩니다.

  • 첫 번째 Depth의 ItemView
  • 두 번째 Depth의 ItemView
  • ExpandableListView의 Adapter


item_view_setting_parent.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/layout_background"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:padding="15dp"
  android:orientation="horizontal">

  <ImageView
    android:id="@+id/icon"
    android:layout_width="32dp"
    android:layout_height="32dp"
    android:layout_marginLeft="16dp"
    android:src="@mipmap/ic_launcher" />

  <TextView
    android:id="@+id/name"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:layout_gravity="center"
    android:text="name"
    android:textColor="@color/black"
    android:textSize="22sp" />

</LinearLayout>


item_view_setting_child.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/layout_background"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:padding="15dp"
  android:orientation="horizontal">

  <TextView
    android:id="@+id/name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="80dp"
    android:layout_gravity="center"
    android:text="name"
    android:textColor="@color/black"
    android:textSize="18sp" />

  <Switch
    android:id="@+id/data_switch"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginRight="18dp"
    android:layout_gravity="right"
    android:checked="true" />

</LinearLayout>


fragment_device_setting.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"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:fitsSystemWindows="false">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/app_bar_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/background"
    android:fitsSystemWindows="true">

    <RelativeLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content">

      <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="12dp"
        android:layout_centerInParent="true"
        android:text="Device Setting"
        android:textColor="@color/white"
        android:textSize="24sp" />

    </RelativeLayout>

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

  <ExpandableListView
    android:id="@+id/expandable_listview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:groupIndicator="@null"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

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


DeviceSettingExpandableAdapter.java

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.HashMap;

public class DeviceSettingExpandableAdapter extends BaseExpandableListAdapter {

  private Context mContext;
  private ArrayList mParentList;
  private HashMap<String, ArrayList> mChildHashMap;

  public DeviceSettingExpandableAdapter(Context context) {
    this.mContext = context;

    initList();
  }

  private void initList() {
    mParentList = new ArrayList<>();
    mParentList.add(
        new SettingParentItem(SettingParentItem.KEY_MYPHONE,
            R.drawable.icon_setting_myphone, "My Phone"));
    mParentList.add(
        new SettingParentItem(SettingParentItem.KEY_SHEALTH,
            R.drawable.icon_setting_shealth, "SHealth"));

    mChildHashMap = new HashMap<>();

    ArrayList childList = new ArrayList<>();
    childList.add(new SettingChildItem(DEVICE_DATA_TYPE.TYPE_MYPHONE_APP_USAGE,
        "앱 사용 시간", ""));
    childList.add(new SettingChildItem(DEVICE_DATA_TYPE.TYPE_MYPHONE_LOCATION,
        "위치 정보", ""));
    childList.add(new SettingChildItem(DEVICE_DATA_TYPE.TYPE_MYPHONE_ACTIVITY,
        "액티비티 정보", ""));
    childList.add(new SettingChildItem(DEVICE_DATA_TYPE.TYPE_MYPHONE_WEATHER,
        "날씨", ""));
    childList.add(new SettingChildItem(DEVICE_DATA_TYPE.TYPE_MYPHONE_EVENT,
        "스마트폰 이벤트", ""));
    mChildHashMap.put(mParentList.get(0).key + "", childList);

    childList = new ArrayList<>();
    childList.add(new SettingChildItem(DEVICE_DATA_TYPE.TYPE_SHEALTH_STEP_COUNT,
        "걸음 수", ""));
    childList.add(new SettingChildItem(DEVICE_DATA_TYPE.TYPE_SHEALTH_SLEEP,
        "수면 정보", ""));
    childList.add(new SettingChildItem(DEVICE_DATA_TYPE.TYPE_SHEALTH_HEART_RATE,
        "심박수", ""));
    mChildHashMap.put(mParentList.get(1).key + "", childList);
  }

  @Override
  public String getGroup(int groupPosition) {
    return mParentList.get(groupPosition).key + "";
  }

  @Override
  public int getGroupCount() {
    return mParentList.size();
  }

  @Override
  public long getGroupId(int groupPosition) {
    return groupPosition;
  }

  // ParentListView
  @Override
  public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
      ViewGroup parent) {
    ParentItemHolder holder = null;
    View row = convertView;

    if(row == null) {
      LayoutInflater inflator = (LayoutInflater) mContext
          .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      row = inflator.inflate(R.layout.item_view_setting_parent, null);

      holder = new ParentItemHolder();

      holder.icon = (ImageView) row.findViewById(R.id.icon);
      holder.name = (TextView) row.findViewById(R.id.name);

      row.setTag(holder);
    } else {
      holder = (ParentItemHolder) row.getTag();
    }

    final SettingParentItem item = mParentList.get(groupPosition);
    if(item != null) {
      holder.icon.setImageResource(item.resId);
      holder.name.setText(item.name);
    }

    return row;
  }

  //ChildListView
  @Override
  public SettingChildItem getChild(int groupPosition, int childPosition) {
    return this.mChildHashMap.get(this.mParentList.get(groupPosition).key + "")
        .get(childPosition);

  }

  @Override
  public int getChildrenCount(int groupPosition) {
    return this.mChildHashMap.get(this.mParentList.get(groupPosition).key + "")
        .size();

  }

  @Override
  public long getChildId(int groupPosition, int childPosition) {
    return childPosition;
  }

  @Override
  public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
      View convertView, ViewGroup parent) {
    ChildItemHolder holder = null;
    View row = convertView;

    if(row == null) {
      LayoutInflater inflator = (LayoutInflater) mContext
          .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      row = inflator.inflate(R.layout.item_view_setting_child, null);

      holder = new ChildItemHolder();

      holder.name = (TextView) row.findViewById(R.id.name);

      row.setTag(holder);
    } else {
      holder = (ChildItemHolder) row.getTag();
    }

    SettingChildItem item = getChild(groupPosition, childPosition);

    if(item != null) {
      holder.name.setText(item.name);
    }

    return row;
  }

  @Override
  public boolean hasStableIds() { return true; }

  @Override
  public boolean isChildSelectable(int groupPosition, int childPosition) { return true; }


  class ParentItemHolder {

    ImageView icon;
    TextView name;
  }

  ;

  class ChildItemHolder {

    TextView name;
    Switch _switch;
  }

  ;
}
</pre>