월별 글 목록: 2014년 12월월

ListView에서 java.lang.IllegalStateException 에러 처리

서비스 하는 앱 중의 하나가 크래시를 리포팅해서 확인해보니, 다음과 같다. 이 앱에서 리포팅한 에러는 아래와 같다. 그리고 이 에러에서 보듯이 리스트뷰(ListView)를 사용하고 있다.

12-12 13:27:04.662: E/AndroidRuntime(15705): java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. Make sure your adapter calls notifyDataSetChanged() when its content changes. [in ListView(2131230758, class android.widget.ListView) with Adapter(class com.xxx.xxx.XXXAdapter)]

구글님께 문의를 한 결과, 화면에 리스트뷰가 보이는 상황에서, 데이터를 추가하는 경우에 발생할 수 있고 아래와 같이 해결하라고 한다.

listview.add(item);
 adapter.notifyDataSetChanged();

or

adapter.add(item)

하지만 같은 에러가 계속 발생했다. 그래서 에러를 트레이스 해보니, 리스트뷰에 있는 layoutChildren() 메서드(리시트뷰 노드가 가지고 있는 하위 트리를 다시 그리는)에서 예외를 발생시킨다. 이 소스에서 예외를 발생시키는 부분을 확인해 보니 다음과 같다.

// Handle the empty set by removing all views that are visible
 // and calling it a day
 if (mItemCount == 0) {
 resetList();
 invokeOnItemScrollListener();
 return;
 } else if (mItemCount != mAdapter.getCount()) {
 throw new IllegalStateException("The content of the adapter has changed but "
 + "ListView did not receive a notification. Make sure the content of "
 + "your adapter is not modified from a background thread, but only from "
 + "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
 + "when its content changes. [in ListView(" + getId() + ", " + getClass()
 + ") with Adapter(" + mAdapter.getClass() + ")]");
 }

이 소스로 mItemCount와 mAdapter.getCount()가 다르면 예외를 발생시키는 것을 알 수 있다. 즉, 위 구글님의 검색결과의 해결책은 맞는 답이다. 그런데 내 경우에는 계속 문제가 발생했다. 디버깅을 해보니, Adapter의 getCount() 메서드가 넘겨주는 값이 다른 값을 넘겨주는 경우가 발생했고 그래서 이 에러가 발생하는 것이었다. 리스트뷰의 스크롤과 클릭 이벤트를 리스너로 등록해서 이벤트가 발생하는 경우에 UI를 처리하기 위해서 UI 스레드로 처리하는 부분들이 있는데 이 영향으로 그런 것도 같다(정확하게 디버깅을 하지 않아서 추측임).

그래서, 내가 사용하는 리스트뷰는 동적으로 데이터를 입력하거나 삭제하지 않는 경우라서, 위 소스를 기준으로 Adapter클래스에서 데이터의 개수를 미리 정해놓고, getCount() 메서드는 정해진 값을 넘겨주도록 했다. 아래가 수정한 소스이다.

public XXXAdapter(Context ctx, int resource, List<Item> items) {
  super(ctx, resource, items);
  this.ctx = ctx;
  this.count = items.size();
  this.inflater = LayoutInflater.from(ctx);
}

@Override
public int getCount() {
  return count;
}

이 소스로 수정한 뒤에 이 에러는 발생하지 않는다.

안드로이드 앱에서 스토어 앱 기능 이용하기

안드로이드 앱 스토어는 많다. 안드로이드 개발자는 플레이 스토어, 아마존 스토어, 티스토어, 올레 마켓, 네이버 스토어 등이 있다는 것을 알고 있을 것이다. 국내는 저조하지만, 그들만의 생태계를 꾸려가고 있는 아마존은 그들의 생태계를 잘 만들고(안드로이드계의 애플(?)) 있는 것 같다. 자 이제 얼마나 많은 안드로이드 앱 스토어가 있는지는 다음의 링크로 확인해 보자.

그래서 안드로이드 앱을 개발하고 서비스 하다 보면, 플레이 스토어와 더불어 몇 개의 앱 스토어에 배포하는 경우가 종종 발생한다. 필자도 개발한 앱을 4개의 앱 스토어에 배포한다. 그래서 앱 스토어별로 바이너리를 빌드해야 한다. 빌드 방법은 다음을 참고해 보자.

본론으로 들어가 보자.
많은 회사가 하나 이상의 앱들을 마켓에 배포하고 있다. 물론 단일 마켓이 아니라 두개 이상의 마켓에 배포를 하는 경우도 많을 것이다. 그래서 아래의 기능을 좀 더 편리하게 지원하는 유틸리티 라이브러리를 개발했다. 기능은 다음과 같다.

  • 마켓 앱을 이용해서 서비스하는 앱을 확인하자
  • 마켓 앱을 이용해서 배포한 회사가 서비스하는 앱들을 확인하자
  • 마켓 앱을 이용해서 앱을 검색해보자
  • 마켓 앱이 없는 경우, 웹으로 위의 기능을 제공하자

이 기능을 사용하면, 앱에서 평가하기(별점주기), 변경내용 보기, 서비스하는 다른 앱 보여주기 등을 스토어 앱을 이용해서 제공할 수 있다. 그래서 아래 마켓을 대상으로 필요한 기능을 개발해 보자.
앱 스토어

이 기능을 개발하기 위해서는 스토어 앱이 제공하는 기능과 인터페이스를 확인해야 한다. 필요한 정보는 아래의 링크를 참고하면 된다.

이제 간단하게 설계를 해 보자. 앱에서 스토어 앱을 사용하는 형태이기에 스토어앱이라는 추상 클래스를 선언하고, 이 클래스를 상속해서 플레이 스토어 앱, 아마존 스토어 앱등을 만들어서 모듈화한다. 이 클래스의 구조는 다음과 같다.

이 구조에서 StoreApp.java와 PlayStoreApp.java를 살펴보고, 이 모듈을 사용하는 예를 보자.

package net.sjava.util.store;

import android.content.pm.PackageManager;

import java.util.Iterator;
import java.util.List;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
/**
 * 앱스토어 클래스 
 * 
 * @author mcsong@gmail.com
 * @date Dec 10, 2014 2:03:53 PM
 * @version 1.0.0
 */
public abstract class StoreApp {
	protected static final String PACKAGE_NAME_PLAY_OLD = "com.google.market";
	protected static final String PACKAGE_NAME_PLAY_NEW = "com.android.vending";
	protected static final String PACKAGE_NAME_AMAZON = "com.amazon.venezia";
	protected static final String PACKAGE_NAME_TSTORE = "com.skt.skaf.A000Z00040";
	protected static final String PACKAGE_NAME_NSTORE = "com.nhn.android.appstore";
	
	protected Intent intent;
	
	private List<ApplicationInfo> getApplications(Context ctx) {
		return ctx.getPackageManager().getInstalledApplications(
				PackageManager.COMPONENT_ENABLED_STATE_DEFAULT);
	}
	
	protected boolean isAppInstalled(Context ctx, String packageName) {
		Iterator<ApplicationInfo> itr = getApplications(ctx).iterator();
		while (itr.hasNext()) {
			if (itr.next().packageName.indexOf(packageName) != -1)
				return true;
		}
		
		return false;
	}
	
	/**
	 * 마켓 앱 설치여부를 리턴한다. 
	 * 
	 * @param ctx
	 * @return
	 */
	public abstract boolean isInstalled(Context ctx);
	
	/**
	 * 앱을 연다.
	 * 
	 * @param ctx
	 * @param uniqueId
	 */
	public abstract void openApp(Context ctx, String uniqueId);
	
	/**
	 * 앱을 검색한다. 
	 * 
	 * @param ctx
	 * @param keyword
	 */
	public abstract void searchApp(Context ctx, String keyword);
}
package net.sjava.util.store;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;

/**
 * 플레이 스토어 클래스 
 * 
 * @author mcsong@gmail.com
 * @date Dec 10, 2014 2:04:13 PM
 * @version 1.0.0
 */
public class PlayStoreApp extends StoreApp {
	static String APP_URL = "http://play.google.com/store/apps/deails?id=";
	static String APP_SEARCH_URL = "http://play.google.com/store/search?q=";
			
	public static PlayStoreApp newInstance() {
		return new PlayStoreApp();
	}
	
	@Override
	public boolean isInstalled(Context ctx) {
		return isAppInstalled(ctx, PACKAGE_NAME_PLAY_OLD)
				|| isAppInstalled(ctx, PACKAGE_NAME_PLAY_NEW);
	}

	@Override
	public void openApp(Context ctx, String uniqueId) {
		if(isInstalled(ctx)) {
			intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + uniqueId));
			ctx.startActivity(intent);
			return;			
		}

		intent = new Intent(Intent.ACTION_VIEW, Uri.parse(APP_URL + uniqueId));
		ctx.startActivity(intent);	
	}
		
	@Override
	public void searchApp(Context ctx, String keyword) {
		if(isInstalled(ctx)) {
			intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q="+ keyword));
			ctx.startActivity(intent);
			return;			
		}

		intent = new Intent(Intent.ACTION_VIEW, Uri.parse(APP_SEARCH_URL + keyword));
		ctx.startActivity(intent);
	}
	
	public void openPublisherApps(Context ctx, String keyword) {
		if(isInstalled(ctx)) {
			intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pub:"+ keyword));
			ctx.startActivity(intent);
			return;			
		}

		intent = new Intent(Intent.ACTION_VIEW, Uri.parse(APP_SEARCH_URL + "pub:" + keyword));
		ctx.startActivity(intent);	
	}
}

소스를 실행하면 다음의 화면을 볼 수 있다.

이 화면에서 버튼을 클릭하면 아래의 로직을 실행한다.

    	// tstore
    	if(vId == R.id.btn_tstore) {
    		//private static String pId = "0000320478";
    		store = TStoreApp.newInstance();
    		//store.openApp(this, "0000320478"); //  
    		store.searchApp(this, "facebook");
    		return;
    	}
    	
    	// amazon
    	if(vId == R.id.btn_amazon) {
    		store = AmazonStoreApp.newInstance();
    		//store.open(this, "com.amazon.mp3"); //  
    		//((AmazonStore)store).search(this, "mp3");
    		store.searchApp(this, "com.amazon.mp3");
    		return;
    	}
    	
    	// naver
    	if(vId == R.id.btn_nstore) {
    		store = NaverStoreApp.newInstance();
    		//store.openApp(this, "409160"); // 
    		store.searchApp(this, "naver");
    		return;
    	}
    }

이상 안드로이드 스토어 앱의 설치 여부를 확인하고, 설치된 경우 스토어 앱을 이용해서 기능을 제공하는 예제를 봤다. 이 소스는 SJava Appstore Util 에서 확인할 수 있다.

최신 소스 및 라이브러리는 https://github.com/mcsong/AppStoreLibrary 에서 확인하시고 사용하면 되겠다.

오래된 PC의 수명을 연장해 보자

오래된 PC의 수명을 연장해보자. 32비트를 사용하는 PC의 수명을 몇 년 또는 그 이상을 사용하기 위한 내용이다. 필자가 사용하는 랩톱은 32비트를 사용하는 오래된 모델이다. 구체적으로 이 모델은 HP의 엘리트북 8440P 이다. 이 랩톱의 기본 사양은 HDD와 메모리 4G를 사용한다. 최근에 이 랩톱을 더 오래 사용하기 위해서 업그레이드하면서 생각나서 적어본다.

오래된 PC의 수명을 늘리기 위해서 하드웨어를 업그레이드하자.

1. 메모리를 충분히 늘리자.

– CPU가 32비트라서, 32비트의 OS를 사용하다 보니, 메모리 제한이 있다. 이런 제한은 PAE가 적용된 커널을 사용해서 해결할 수 있으니 보드가 허용하는 메모리를 최대한 추가하자.

2. HDD를 SSD로 교체하자.

– PC에서 가장 느린 부분인 HDD를 SSD로 교체하자. 이제 SSD의 가격이 많이 저렴해져서 HDD를 SSD로 교체하는 것이 좋겠다.

위에서 하드웨어를 업그레이드를했다면, 아래의 과정으로 운영체제를 업그레이드하자.

1. 윈도를 사용하고 있다면 32비트 윈도(Vista/7/8/8.1) 에서 4G 이상 메모리 사용하기를 참고해서 PAE를 적용해서 메모리를 충분히 활용하자. 리눅스 계열은 PAE가 적용된 커널 설치가 어렵지 않으니 찾아서 설치하면 된다.
2. SSD의 경우에는 Trim 기능이 필요하기 때문에, 윈도의 경우 7 이전에는 Trim용 툴을 설치해 줘야 한다. 윈도 8 이후부터는 Trim 기능이 내장되어 있다.

이 과정으로 32비트를 사용하는 오래된 PC는 5년 이상 더 사용할 수 있겠다.

32비트 윈도(Vista/7/8/8.1) 에서 4G 이상 메모리 사용하기

아래 글은 최근 윈도우 업데이트로 인해서 에러가 발생합니다.
아래에서 사용하는 툴의 업데이트를 확인해서 반영되는 대로 이 메세지는 삭제하겠습니다.

—————-

32bit 윈도를 사용하는 기기에 4G 이상의 메모리를 사용하고 있다면 보통은 3G 정도를 사용할 수 있다고 나올 것이다. 많은 기기에 32bit CPU가 장착되어 있지만, 4G 이상의 메모리가 장착된 기기를 쉽게 볼 수 있다. 그래서 PAE 패치는 32bit 기반의 기기에서 메모리를 최대한 활용하기 위해서 꼭 필요한 것이다.

wj32.org 라는 블로그를 운영하시는 분이 윈도 Vista/7/8/8.1에서 PAE를 적용할 수 있도록 툴을 제공하고 있다. 윈도 8.1까지 적용할 수 있는 툴은 http://wj32.org/wp/2013/10/25/pae-patch-updated-for-windows-8-1/ 에서 다운받을 수 있다.

위에서 다운받은 툴의 readme.txt 파일의 순서대로 패치를 하면 다음과 같다.
아래 순서는 C 드라이브에 윈도 8.1이 설치된 필자의 랩톱에서 사용한 예제이다.

# 패치하기
1. 다운받은 툴의 PatchPae2.exe를 Windows/System32 폴더에 복사를 한다.
2. cmd를 관리자 권한으로 실행한다.
3. 128 GB 까지 메모리를 인식할 수 있도록 커널을 패치한다.
3.1 윈도 8/8.1 : c:\Windows\System32>PatchPae2.exe -type kernel -o ntoskrnx.exe ntoskrnl.exe
3.2 윈도 Vista/7 : c:\Windows\System32>PatchPae2.exe -type kernel -o ntkrnlpx.exe ntkrnlpa.exe
4. c:\Windows\System32>PatchPae2.exe -type loader -o winloadp.exe winload.exe
– 로더의 사인 검증 로직을 제거한다.
5. bcdedit /copy {current} /d “Windows (PAE Patched)”
– 부트 항목을 추가한다.
– 이 명령을 실행하면, {8bfc9ccb-7075-11e4-8f2b-9980ef68eed0}와 같은 UUID를 확인할 수 있다.
6. 패치된 커널의 추가한 부트 항목으로 로딩하기 위해서 아래의 명령을 실행한다.
6.1 윈도 8/8.1 : c:\Windows\System32>bcdedit /set {8bfc9ccb-7075-11e4-8f2b-9980ef68eed0} kernel ntoskrnx.exe
6.2 윈도 Vista/7 : c:\Windows\System32>bcdedit /set {8bfc9ccb-7075-11e4-8f2b-9980ef68eed0} kernel ntkrnlpx.exe
7. c:\Windows\System32>bcdedit /set {8bfc9ccb-7075-11e4-8f2b-9980ef68eed0} path \Windows\system32\winloadp.exe
– 패치된 로더로 변경한다.
8. c:\Windows\System32>bcdedit /set {8bfc9ccb-7075-11e4-8f2b-9980ef68eed0} nointegritychecks 1
– 로더 검증 로직을 제거한다.
9. c:\Windows\System32>bcdedit /set {bootmgr} default {8bfc9ccb-7075-11e4-8f2b-9980ef68eed0}
– 추가한 부트 항목을 디폴트로 잡아준다.
10. c:\Windows\System32>bcdedit /set {bootmgr} timeout 2
– 부트 로더가 항목을 보여주는 시간을 설정한다. 범위는 0-999 까지고 위에서는 2초로 설정하고 있다.
11. 컴퓨터를 재시작한다.
– 아래의 화면으로 32bit 윈도 8.1이 4G의 메모리를 인식하는 것을 알 수 있다.

# 삭제하기
1. PAE 패치하지 않은 윈도로 부팅을 한다.
2. msconfig 툴을 실행해서 boot 메뉴에서 “Windows (PAE Patched)” 항목을 삭제한다.
3. Windows/System32 폴더에서 커널(ntoskrnx.exe 또는 ntkrnlpx.exe) 하고 winloadp.exe를 삭제한다.

# 업데이트시
– 윈도 커널의 업데이트시에는 위의 3.번 이후의 과정을 통해서 PAE 패치를 한다.

이 과정으로 32bit 버전의 기기에서 4G가 넘는 메모리를 윈도에서 인식해서 사용할 수 있겠다.