Android에서 Bitmap 관련 작업을 할때 항상 사용하는것이 Bitmap 클래스와 BitmapFactory 클래스이다.

BitmapFactory 클래스는 decode 메서드를 사용하여 File, Stream 등을 입력받아 Bitmap으로 변환할 수 있다.
Bitmap 클래스는 Bitmap의 재가공, Bitmap의 구성을 변경한다던지, 크기를 변경하는 작업을 수행한다.

그런데 현재 Android 상에서 위 2개는 심각한 메모리 누수를 유발하고 있다.

단순 SD 카드에서 파일을 읽어 와 표시해주는 것이라면 관계가 없지만 

작업 1. MediaStore를 사용하여 이미지를 가지고 온 후 크기를 변경하고 이를 화면에 표시함과 동시에 서버에 업로드 한다.

위 작업이 한번이 아닌 여러번 수행 되어야 한다면..? 3번? 많게는 5번 이내로 오류나서 어플이 종료가 된다.;;

오류를 발생한 작업 

		Bitmap resized = null;
		Bitmap orgBitmap = null;
		File targetFile = null;
		int[] size = null;

		// 이미지 품질 옵션
		options.inTempStorage = new byte[16*1024];
		options.inSampleSize = 1;

		switch(requestCode){
			case CommonResource.SHOW_ALBUM:
				photoUri = data.getData();
				
				// 갤러리에서 선택했을때 실제 경로 가져오기 위해 커서
				String [] proj={MediaStore.Images.Media.DATA};  
			    Cursor cursor = managedQuery( photoUri,  
			            proj, // Which columns to return  
			            null,       // WHERE clause; which rows to return (all rows)  
			            null,       // WHERE clause selection arguments (none)  
			            null); // Order-by clause (ascending by name)  
			    int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);  
			    cursor.moveToFirst();  
			  
			    photoUri = Uri.parse(cursor.getString(column_index));
			    // 비트맵을 먼저 만들고
			    orgBitmap = BitmapFactory.decodeFile(photoUri.getPath(), options);
			    // 원본 사이즈에 따른 새로운 사이즈 계산
			    size = commonUtil.getBitmapSize(orgBitmap);
			    // 비트맵 생성
			    displayBitmap = Bitmap.createScaledBitmap(orgBitmap, size[0], size[1], true);
			    
			    // 디바이스에 맞게 비트맵 생성 후 업로드 하여야 하므로 임시 파일을 생성해준다.
			    // MediaStore에는 원본이, 아래는 복사본을 생성하여 업로드 하는 것.
			    try {
					File targetFolder = new File(targetPath);
					if(!targetFolder.exists()){
						targetFolder.mkdirs();
						targetFolder.createNewFile();
					}
					if(targetFile == null)
						targetFile = new File(targetFolder, UUID.randomUUID().toString());
					
					FileOutputStream out = new FileOutputStream(targetFile);
					displayBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); 
					out.flush();
					out.close();
					
					uploadImagePath = targetFile.getAbsolutePath();
					
				} catch (Exception e) { 
				       e.printStackTrace(); 
				} finally{
					targetFile = null;
					uploadImage.setImageBitmap(commonUtil.getRoundedCornerBitmap(this, displayBitmap, 10));
				}
			    break;
			}

위 소스에서 orgBitmap에 앨범에서 사진을 하나 불러온다. 이 사진은 사이즈가 허벌나다. (orgBitmap)
그리고 이 사진을 그대로 업로드 할 시 트래픽도 크고 시간도 오래 걸릴게 뻔하다 생각하여 
비율에 맞게 다시 크기를 조정하여 비트맵을 생성하게 했다. (displayBitmap)
그리고 업로드를 위한 임시파일을 생성하도록 코드가 이어지고
마지막으로 화면에 표시할때는 round 처리까지 한 후 보여지게 되어 있다.

그지같게도 비트맵을 생성한 후 메모리가 반환이 되지 않는다. 
절대... 쓸일 없는 Bitmap은 recycle()하면 된다지만 역시 일부다.

위 작업을 두번하니까 어플이 그냥 죽는다. 카메라로 촬영을 하든 앨범에서 가지고 오든 죽는다.

원인은 비트맵의 반복 생성. 
createScale을 사용하던 decode를 하던 한번 만드는 것도 메모리를 상당부분 사용하는데 이를 중복으로 사용하는게 문제라는 것.

그래서 아래와 같이 코드를 바꾸기로 했다.

		// 이미지 품질 옵션
		options.inJustDecodeBounds = true;
		
		switch(requestCode){
			case CommonResource.SHOW_ALBUM:
				photoUri = data.getData();
					
				// 갤러리에서 선택했을때 실제 경로 가져오기 위해 커서
				String [] proj={MediaStore.Images.Media.DATA};  
			    Cursor cursor = managedQuery( photoUri,  
			            proj, // Which columns to return  
			            null,       // WHERE clause; which rows to return (all rows)  
			            null,       // WHERE clause selection arguments (none)  
			            null); // Order-by clause (ascending by name)  
			    int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);  
			    cursor.moveToFirst();  
			  
			    photoUri = Uri.parse(cursor.getString(column_index));
			    uploadImagePath = photoUri.getEncodedPath();
			    displayBitmap = BitmapFactory.decodeFile(uploadImagePath, options);
			    
			    options = commonUtil.getBitmapSize(options);
			    displayBitmap = BitmapFactory.decodeFile(uploadImagePath, options);
				
			    uploadImage.setImageBitmap(displayBitmap);
				
			    break;
		}

// http://kfb-android.blogspot.com/2009/04/image-processing-in-android.html#viewSource
	public Options getBitmapSize(Options options){
		int targetWidth = 0;
		int targetHeight = 0;
		
		if(options.outWidth > options.outHeight){	
	    	targetWidth = (int)(600 * 1.3);
	    	targetHeight = 600;
	    }else{
	    	targetWidth = 600;
	    	targetHeight = (int)(600 * 1.3);
	    }

		Boolean scaleByHeight = Math.abs(options.outHeight - targetHeight) >= Math.abs(options.outWidth - targetWidth);
		if(options.outHeight * options.outWidth * 2 >= 16384){
		    double sampleSize = scaleByHeight
		        ? options.outHeight / targetHeight
		        : options.outWidth / targetWidth;
		    options.inSampleSize = (int) Math.pow(2d, Math.floor(Math.log(sampleSize)/Math.log(2d)));
		}
		options.inJustDecodeBounds = false;
		options.inTempStorage = new byte[16*1024];
		
		return options;
	}
Bitmap으로 생성하진 않고 먼저 decoding만 한 후 크기를 가져와 실제 맞추려는 크기와 비교하여 size 조절이 필요하면
사이즈를 조절하도록 option을 설정하면 된다.
구글링 하다 보니 누가 만들어 놓았더라. (출처는 소스에..)

아.. 맘 놓고 해결..

서버와 통신을 JSON을 통해서 하고 있는데 ORM을 사용한 클래스 구조라 굴비 엮듯이 데이터가 엮일때가 있다.

이미지 4개의 정보를 가지고 오는데 JSON을 통해 받은 글자수가 무려 194만... 이러니 메모리가 없을수밖에..

뺄거 다 빼고 해보니 1만자도 안된다. 작게 따져도 194배 줄였다.

혼자 급하게 개발하다 보니 결과만 나오면 될때가 있는데 이런일이 비일비재하다..

욕나온다 ㅅㅂ

	public Bitmap getRoundedCornerBitmap(Context context, Bitmap bitmap , int roundLevel) { 
	    Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888); 
	    Canvas canvas = new Canvas(output); 
	 
	    final int color = 0xff424242; 
	    final Paint paint = new Paint(); 
	    final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); 
	    final RectF rectF = new RectF(rect); 
	    final float roundPx = convertDipsToPixels(context, roundLevel); 
	 
	    paint.setAntiAlias(true); 
	    canvas.drawARGB(0, 0, 0, 0); 
	    paint.setColor(color); 
	    canvas.drawRoundRect(rectF, roundPx, roundPx, paint); 
	 
	    paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); 
	    canvas.drawBitmap(bitmap, rect, rect, paint); 
	 
	    return output; 
	} 
	
	public static int convertDipsToPixels(Context context, int dips) { 
	    final float scale = context.getResources().getDisplayMetrics().density; 
	    return (int) (dips * scale + 0.5f); 
	}

roundLevel을 0~100까지 조절할 수 있다.
이어서...

Layout에 TextView를 추가하겠습니다.
그리드 뷰에서 스크롤이 이동될때 관련 메세지가 찍히도록 하기 위해서 아래처럼 수정합니다.

[File : main.xml]

	
	
 20개의 이미지를 대상으로 했으나 이제 40개로 하겠습니다.

아래 20개의 URL을 url.xml에 추가해줍니다.
[File : url.xml]

	
			   
	   
	   
			   
	
	
	
			   
	
	
	
	
	
	
	
	
	
	
	


그리고 Activity를 조금 손 보겠습니다.

[File : ImageList.java]
package com.yeory.gv;

import java.io.IOException;
import java.util.ArrayList;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import android.app.Activity;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.GridView;
import android.widget.TextView;
import android.widget.Toast;

import com.yeory.gv.common.CommonResources;
import com.yeory.gv.model.Image;
import com.yeory.gv.util.CommonUtil;
import com.yeory.gv.util.ImageAdapter;
import com.yeory.gv.util.ImageDownloader;

/**
 * =============================================================================
/            프로젝트명 :   GridView_Example
/            화  일  명 :   ImageList.java
/            기      능 :   
/            인      수 :   
/            특이  사항 :
/-----------------------------------------------------------------------------
/                               변경 사항				                     
/-----------------------------------------------------------------------------
/    변경일자       	변경자(작성자)                 		변경 내역                 
/   ----------     	--------------------------       -------------------------
/   2010. 11. 10.      jYeory<jyeory@gmail.com>         		최 초 작 성                      
/==============================================================================
 */
public class ImageList extends Activity {
	private final ImageDownloader imageDownloader = ImageDownloader.getInstance();
	
	private GridView gridView;
	private TextView textView;
	private ImageAdapter adapter;
	
	private boolean isNextPage = true;
	boolean needCached = true;
	final int size = 15;			// 한번에 보여줄 이미지 개수
	private int totalSize = 0;		// paging에 필요
	private int currentPage = 0;	// paging에 필요
	private int start = 0;			
	private int end = 0;
	private int endPage = 0;
	private int lastPosition = 0;
	private int firstPosition = 0;
	
	private ProgressDialog pd;
	private Thread thread;
	
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		if(keyCode == 4) CommonUtil.getInstance().requestKillProcess(this);
		return super.onKeyDown(keyCode, event);
	}
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        CommonResources.COMMON_CONTEXT = this;
        
        init(1, size);
        
        textView = (TextView) findViewById(R.id.logText);
        
        gridView = (GridView) findViewById(R.id.gridView);
        adapter = new ImageAdapter(this, imageList);
        
        gridView.setOnScrollListener(new OnScrollListener() {
			@Override
			public void onScrollStateChanged(AbsListView view, int scrollState) {
				isNextPage = imageList.size() < totalSize;
				try{
					switch (scrollState) {
					case 1:
						lastPosition = view.getLastVisiblePosition();
						firstPosition = view.getFirstVisiblePosition();
						if( lastPosition == (imageList.size()-1) && isNextPage ){
							pd = ProgressDialog.show(ImageList.this,  null, "데이터 수신중 입니다.\n잠시만 기다려 주세요...", false, true);
							
							thread = new Thread(new Runnable() {
								public void run() {
									start = (++currentPage - 1) * size;
									if(start < totalSize){
										endPage = ((currentPage*size) < totalSize)?(currentPage*size):totalSize;
										imageList.addAll(wholeImageList.subList(start, endPage));
									}
									// 아래 코드를 주석처리할 시 AsyncTask를 확인할 수 있다.
//									downloadImages(imageList);
									handler.sendEmptyMessage(0);
								}
							});
							thread.start();
						}
						break;
					default:
						break;
					}
				}catch(Exception e){
					Log.i("error", "gridView.setOnScrollListener : "+e.getCause().toString());
				}
			}
			
//			firstVisibleItem : 화면 최상단 이미지의 IDX
//			visibleItemCount : 현재 화면에 보여지는 이미지 개수
			@Override
			public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
				textView.setText("onScroll : "+"view.getLastVisiblePosition() - "+view.getLastVisiblePosition()+" / "+totalItemCount);
			}
		});
        
        gridView.setAdapter(adapter);
        
        Toast.makeText(this, "Yeory - www.yeroy.com", Toast.LENGTH_LONG).show();
    }
    
    /**
	 * ProgressDialog 표시 중 해당 이미지들을 다운받는다.
	 * AsyncTask를 사용할 경우 호출할 필요 없음. 
	 * @param images
	 */
	private void downloadImages(ArrayList images){
		for(Image image : images){
			imageDownloader.callDownloadImage(image.getThumbNail());
		}
	}
	
	private Handler handler= new Handler() {
		public void handleMessage(Message msg) {
			if(adapter == null) adapter = new ImageAdapter(ImageList.this, imageList);
			else{
				adapter.setList(imageList);
			}
			gridView.setAdapter(adapter);
			
			gridView.setSelection(firstPosition);
			pd.dismiss();
		}
	};
    
    private ArrayList wholeImageList = new ArrayList();	// 전체 이미지 담을 것.
	private ArrayList imageList = new ArrayList();	// 부분 이미지 담을 것.
	private XmlPullParser xpp;
	
    /**
	 * XML을 읽어들여 전체 이미지를 ArrayList에 넣는다.
	 * 처음으로 호출 되기에 15개만 다른 list에 담아 캐싱. 
	 * @param page
	 * @param size
	 */
	private void init(final int page, final int size) {
		xpp = getResources().getXml(R.xml.url);
		try{
			while ( xpp.getEventType() != XmlPullParser.END_DOCUMENT ){  // 마지막 문서까지
				if ( xpp.getEventType() == XmlPullParser.START_TAG ){
					if ( xpp.getName().equals("image") ){
						wholeImageList.add(new Image(xpp.getAttributeValue(0), xpp.getAttributeValue(1), xpp.getAttributeValue(2)));
					}
				}
				xpp.next();
			}
			
			totalSize = wholeImageList.size();
			currentPage = page;

			start = (page - 1) * size; 
			end = (page*size);
			imageList.addAll(wholeImageList.subList(start, end));
			
		}catch(XmlPullParserException xppe){
			Log.i("error", "init()"+xppe);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

새롭게 그리드 뷰 스크롤에 관련된 이벤트가 추가되었습니다.
또한 이미지 캐시 기능을 제공하는 클래스인 ImageDownloader도 추가되어있습니다.

먼저 ImageDownloader는 아래 주소에 번역된 글이 있으니 참조 바랍니다.
번역 : 
http://huewu.blog.me/110090363656
원본 : http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html

저는 위 코드를 입맛에 맞게 뜯어고쳐(?)서 사용하고 있습니다.

ImageDownloader.java의 코드는 너무 길어 적지 않고 글의 흐름상 필요한 메서드만 기술합니다.

Activity에서 GridView의 adapter를 set해주는 곳이 있었는데 실제 그 부분은 ImageAdapter.java 였습니다.
캐시 기능을 위해 아래처럼 변경되었습니다.

[File : ImageAdapter.java]
package com.yeory.gv.util;

import java.util.ArrayList;

import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;

import com.yeory.gv.model.Image;
import com.yeory.gv.util.ImageDownloader.Mode;
/**
 * =============================================================================
/            프로젝트명 :   GridView_Example
/            화  일  명 :   ImageAdapter.java
/            기      능 :   
/            인      수 :   
/            특이  사항 :
/-----------------------------------------------------------------------------
/                               변경 사항				                     
/-----------------------------------------------------------------------------
/    변경일자       	변경자(작성자)                 		변경 내역                 
/   ----------     	--------------------------       -------------------------
/   2010. 11. 10.      jYeory<jyeory@gmail.com>         		최 초 작 성                      
/==============================================================================
 */
public class ImageAdapter extends BaseAdapter{
	private final ImageDownloader imageDownloader = ImageDownloader.getInstance();
	private final String LOG_TAG = "ImageAdapter";
	private Context mContext;
    private ImageView imageView;
    private ArrayList list;
    
    public ImageAdapter(Context c) {
        mContext = c;
    }
    
    public ImageAdapter(Context c, ArrayList list){
    	mContext = c;
    	this.setList(list);
    }
	
	
	@Override
	public int getCount() {
		Log.i(LOG_TAG, "called getCount()");
		return getList().size();
	}

	@Override
	public Object getItem(int position) {
		Log.i(LOG_TAG, "called getItem()");
		return null;
	}

	@Override
	public long getItemId(int position) {
		Log.i(LOG_TAG, "called getItemId()");
		return 0;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		Log.i(LOG_TAG, "called getView()");
		
		if (convertView == null) {  // if it's not recycled, initialize some attributes.
    		imageView = new ImageView(mContext);
    		imageView.setLayoutParams(new GridView.LayoutParams(150, 150));
    		imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
    		imageView.setPadding(4, 4, 4, 4);
        } else {
            imageView = (ImageView) convertView;
        }
		
		imageDownloader.setMode(Mode.CORRECT);
		imageDownloader.download(getList().get(position).getThumbNail(), imageView);
		return imageView;
	}

	public void setList(ArrayList list) {
		this.list = list;
	}

	public ArrayList getList() {
		return list;
	}
}
코드에 살펴보시면 Mode.CORRECT 가 있는데 이 코드가 Async로 할 것인지에 대한 값입니다.

CORRECT로 할 경우 각 이미지를 개별로 다운받아 바로 사용자에게 표시합니다.
(주의 : ImageList.java에서 PrgoressDialog 표시 중일때 downloadImages(imageList); 를 주석처리 하여야 합니다.)

즉 CORRECT로 한다면 20개의 이미지를 먼저 다운로드 되는 것부터 표시할 수 있고,
주석처리할 경우 20개의 이미지가 모두 다운로드 된 후에야 사용자에게 표시하는 차이가 있습니다.

코드가 너무 많아져서 다 올리지 못하여 역시나 소스를 첨부합니다.
캐시 기능을 하는 것에 대해서는 해당 링크를 참조하시면 될 듯합니다.




다음은 그리드 뷰에서 아이템 클릭시 크게 보는 것과 드래그입니다...

  1. 2011.03.11 10:26

    비밀댓글입니다

  2. 안드로메이드 2012.02.15 11:47 신고

    그리드뷰를 어떻게 페이징으로 구현 할지 몰랐는데
    도움이 많이 되었습니다
    감사합니다 ^^

  3. 안드로이드 2012.10.17 15:55 신고

    소스다운받아서 올리면 앱이 죽네요 ㅠ

  4. 마앙 2012.11.12 13:19 신고

    썸네일 클릭시 확대하는건 언제 수정하시나요 ㅜㅠ? 제가 필요해서,....
    거기에 뷰플리퍼까지 넣는 기능을 넣어야 되는데 힘드네요

  5. 감사합니다 2013.07.06 20:01 신고

    이런 소스를 공개하긴 쉽지않을텐데. 덕분에 처리할 수 있을 것 같습니다.
    감사합니다.

    • 이직과 업무 변경으로 현재 포스트 이후 써내려 가야할 소스가 사라져 더이상 포스팅을 못하는게 아쉽네요..

      도움 되셨다면 그걸로 만족입니다.

  6. 감사합니다 2013.07.14 17:16 신고

    그런데...제가 한 100여개의 인터넷 url 을 받아와서 뿌려주고있는데요

    스크롤을 막내리다보면 죽어버리는 경우가 있습니다.(과격하게 사용하는경우)
    어찌해야할까요..........................ㅠㅠ

    현재 100장이미지인데 페이지를 100장을 초기값 잡았습니다.
    그래서 스크롤 없이 한번에 다보여준식으로....일단 이렇게 쓰면될거같긴한데

    혹시 페이지넘길때 생기는방식(카카오 스토리와같이)
    이 안정적으로 되는 추가방법 생각나시면 댓글 부탁드려요 (__*

    • 캐시를 이용하시나요??
      제 경우엔 Web 이미지를 표시를 해야할 때 캐시가 필수라 생각했었어요.

      1 ~ 20 까지 처음이고 스크롤을 내릴 시 5개씩의 이미지를 추가로 로드한다고 할 때,

      6 ~ 25 까지 이미지를 표시하고 (스크롤을 한번 내린 상태)
      기존 1 ~5의 이미지를 다시 보고자 스크롤을 올릴때 캐시를 이용하지 않으면 또 URL을 이용해서 이미지를 가지고 왔었습니다.
      이게 계속적으로 반복이 되면 메모리 부족으로 앱이 사망했구요.

      캐시를 이용할 경우로 500장까지 스크롤 테스트 했지만 문제 없었고...
      리스트에 표시되는 이미지도 미리 로컬로 다운로드 된 이미지를 가지고 오는 것이기에 즉각적으로 표시 되었구요.

  7. 감사합니다 2013.07.16 15:52 신고

    올려주신 소스가 캐시 사용을 기본으로 하는게 아닌가요?
    지금 경우 100장을 기준으로 한다고해도 3G 환경에서 실험결과,
    한 페이지내에 그림이 몇개 뜨다가 제가 스크롤 내렸다가 다시 돌아오면 전부 다시 로딩하는거같던데요....

    주석처리하는부분이 캐싱인가요?

    • 기본적으로 캐시를 합니다.
      다운로드 디렉토리만 정상적이라면 거기에 이미지를 넣어두고 가져오니깐요..

      제가 테스트 한 결과는
      첫 20여개의 이미지 로딩 완료 되고 스크롤 이동 후 다시 표시하고자 할때는 캐시된 이미지가 나왔기 때문에 바로 화면에 표시가 되었구요.

      주석처리 하는 부분은 화면상의 이미지를 한꺼번에 다 보여줄것이냐,
      다운로드 완료 된 것부터 보여줄 것이냐 하는 부분이기 때문에 해당 부분은 아닌 것 같네요..

      이미지도 썸네일을 불러오시는거라면.. 제 코드에 문제가 있을거라 보여지네요.

getNetworkOperatorName()는 통신망 사업자를 알려준다.
아래 그림과 같이 KT, SKT 다나오는데 LG는 저딴식으로 나오더라..
당황했다..
다 뒤져봐도 표시할 방법이 없다.
LG 폰이 하나 둘 등록되면 그때 다시 알아봐야 할 문제인거 같다.



	//reqired permisson
	TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); 

	provider = tm.getNetworkOperatorName();

	Log.i("check", "Build.BOARD : "+Build.BOARD);
	Log.i("check", "Build.BRAND : "+Build.BRAND);
	Log.i("check", "Build.CPU_ABI : "+Build.CPU_ABI);
	Log.i("check", "Build.DEVICE : "+Build.DEVICE);
	Log.i("check", "Build.DISPLAY : "+Build.DISPLAY);
	Log.i("check", "Build.FINGERPRINT : "+Build.FINGERPRINT);
	Log.i("check", "Build.HOST : "+Build.HOST);
	Log.i("check", "Build.ID : "+Build.ID);
	Log.i("check", "Build.MANUFACTURER : "+Build.MANUFACTURER);
	Log.i("check", "Build.MODEL : "+Build.MODEL);
	Log.i("check", "Build.PRODUCT : "+Build.PRODUCT);
	Log.i("check", "Build.TAGS : "+Build.TAGS);
	Log.i("check", "Build.TYPE : "+Build.TYPE);
	Log.i("check", "Build.USER : "+Build.USER);

귀찮은 분들을 위한 코드 포탈..


private Bundle bundleResult=new Bundle();
private JSONObject JSONObj;
private JSONArray JSONArr;
Private SoapObject resultSOAP = (SoapObject) envelope.getResponse();
/* gets our result in JSON String */
private String ResultObject = resultSOAP.getProperty(0).toString();

if (ResultObject.startsWith("{")) { // if JSON string is an object
    JSONObj = new JSONObject(ResultObject);
    Iterator itr = JSONObj.keys();
    while (itr.hasNext()) {
        String Key = (String) itr.next();
        String Value = JSONObj.getString(Key);
        bundleResult.putString(Key, Value);
        // System.out.println(bundleResult.getString(Key));
    }
} else if (ResultObject.startsWith("[")) { // if JSON string is an array
    JSONArr = new JSONArray(ResultObject);
    System.out.println("length" + JSONArr.length());
    for (int i = 0; i < JSONArr.length(); i++) {
        JSONObj = (JSONObject) JSONArr.get(i);
        bundleResult.putString(String.valueOf(i), JSONObj.toString());
        // System.out.println(bundleResult.getString(i));
    } 
}
원문 : http://sixarm.com/about/phone-review-nexus-s-ugly-buggy-slow.html

한줄 결론..  디자인 후지고, 버그 투성에 느리다
오류 :  java.lang.IllegalArgumentException: Illegal character in path at index xx


안드로이드에서는 URL을 인식할때 공백을 인식하지 못한다.
URL이 지정되어 있다면(또는 지정된 URL을 입력한다면) 애초에 공백을 %20으로 바꾸어 주어야 한다.

그것이 아니라면 아래 링크에 게재된 코드를 사용하는 수 밖에 없다.


import java.net.ProtocolException;
import java.net.URI;
import java.net.URISyntaxException;

import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.CircularRedirectException;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.impl.client.DefaultRedirectHandler;
import org.apache.http.impl.client.RedirectLocations;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;

public class CustomRedirectHandler extends DefaultRedirectHandler{

	private static final String REDIRECT_LOCATIONS = "http.protocol.redirect-locations";

	public CustomRedirectHandler() {
		super();
	}

	public boolean isRedirectRequested(
			final HttpResponse response,
			final HttpContext context) {
		if (response == null) {
			throw new IllegalArgumentException("HTTP response may not be null");
		}
		int statusCode = response.getStatusLine().getStatusCode();
		switch (statusCode) {
		case HttpStatus.SC_MOVED_TEMPORARILY:
		case HttpStatus.SC_MOVED_PERMANENTLY:
		case HttpStatus.SC_SEE_OTHER:
		case HttpStatus.SC_TEMPORARY_REDIRECT:
			return true;
		default:
			return false;
		} //end of switch
	}

	public URI getLocationURI(final HttpResponse response, final HttpContext context) {
		URI uri = null;
		try{
			if (response == null) {
				throw new IllegalArgumentException("HTTP response may not be null");
			}
			//get the location header to find out where to redirect to
			Header locationHeader = response.getFirstHeader("location");
			if (locationHeader == null) {
				// got a redirect response, but no location header
				throw new ProtocolException("Received redirect response " + response.getStatusLine()+ " but no location header");
			}
			//HERE IS THE MODIFIED LINE OF CODE
			String location = locationHeader.getValue().replaceAll (" ", "%20");

			try {
				uri = new URI(location);            
			} catch (URISyntaxException ex) {
				throw new ProtocolException("Invalid redirect URI: " + location);
			}

			HttpParams params = response.getParams();
			// rfc2616 demands the location value be a complete URI
			// Location       = "Location" ":" absoluteURI
			if (!uri.isAbsolute()) {
				if (params.isParameterTrue(ClientPNames.REJECT_RELATIVE_REDIRECT)) {
					throw new ProtocolException("Relative redirect location '"+ uri + "' not allowed");
				}
				// Adjust location URI
				HttpHost target = (HttpHost) context.getAttribute(
						ExecutionContext.HTTP_TARGET_HOST);
				if (target == null) {
					throw new IllegalStateException("Target host not available " + "in the HTTP context");
				}

				HttpRequest request = (HttpRequest) context.getAttribute(
						ExecutionContext.HTTP_REQUEST);

				try {
					URI requestURI = new URI(request.getRequestLine().getUri());
					URI absoluteRequestURI = URIUtils.rewriteURI(requestURI, target, true);
					uri = URIUtils.resolve(absoluteRequestURI, uri); 
				} catch (URISyntaxException ex) {
					throw new ProtocolException();
				}
			}

			if (params.isParameterFalse(ClientPNames.ALLOW_CIRCULAR_REDIRECTS)) {

				RedirectLocations redirectLocations = (RedirectLocations) context.getAttribute(
						REDIRECT_LOCATIONS);

				if (redirectLocations == null) {
					redirectLocations = new RedirectLocations();
					context.setAttribute(REDIRECT_LOCATIONS, redirectLocations);
				}

				URI redirectURI;
				if (uri.getFragment() != null) {
					try {
						HttpHost target = new HttpHost(
								uri.getHost(), 
								uri.getPort(),
								uri.getScheme());
						redirectURI = URIUtils.rewriteURI(uri, target, true);
					} catch (URISyntaxException ex) {
						throw new ProtocolException(ex.getMessage());
					}
				} else {
					redirectURI = uri;
				}

				if (redirectLocations.contains(redirectURI)) {
					throw new CircularRedirectException("Circular redirect to '" +redirectURI + "'");
				} else {
					redirectLocations.add(redirectURI);
				}
			}
		}catch(ProtocolException ex){
			ex.printStackTrace();
		} catch (CircularRedirectException e) {
			e.printStackTrace();
		}
		return uri;
	}
}

사용법은 아래와 같이...

		CustomRedirectHandler redirectHandler = new CustomRedirectHandler();
		client = new DefaultHttpClient();
		((AbstractHttpClient) client).setRedirectHandler(redirectHandler);



Tab 구성.

		TabSpec ts1 = tab_host.newTabSpec("today");
		ts1.setIndicator("Today");
		ts1.setContent(imageView.getId());
		tab_host.addTab(ts1);

		TabSpec ts2 = tab_host.newTabSpec("favorite");
		ts2.setIndicator("Favorites");
		initFavoriteViewTab();
		ts2.setContent(R.id.secondTab);
		tab_host.addTab(ts2);


	
	private final int SEARCH = 100;
	private final int REMOVE = 200;

	@Override
	public boolean onPrepareOptionsMenu(Menu menu){
		Log.w("check", "tab_host.getCurrentTab(); : "+tab_host.getCurrentTab());
		Log.w("check", "tab_host.getCurrentTabTag() : "+tab_host.getCurrentTabTag());
		Log.w("check", "tab_host.getCurrentView() : "+tab_host.getCurrentView().getContext().toString());
		
		menu.clear();
		MenuItem itemAdd;
		int tabIdx = tab_host.getCurrentTab();
		switch (tabIdx) {
		case 0:
			itemAdd = menu.add(0, SEARCH, Menu.NONE, "검색").setIcon(android.R.drawable.ic_menu_search);
			itemAdd.setShortcut('0', 'a');
			break;
		case 1:
			itemAdd = menu.add(0, REMOVE, Menu.NONE, "제거").setIcon(android.R.drawable.ic_delete);
			itemAdd.setShortcut('1', 'b');
			break;
		}
		return true;
	}

onPrepareOptionsMenu에서 탭의 Tag 또는 index를 이용하여 각 탭에 맞게 메뉴를 구성할 수 있다.


각 메뉴의 클릭 처리 역시 itemId를 이용하여 로직을 구성해준다.
	
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		super.onContextItemSelected(item);
		Log.w("check", "item.getItemId() : "+item.getItemId());
		switch(item.getItemId()){
			case (SEARCH):{
				// do something
			}
			case (REMOVE) :
				// do something
			break;
		}
		return false;
	}
다 뒤져 보았는데 정말 이보다 좋은 튜토리얼은 존재하지 않았다....



소스코드 또한 다운로드 가능하다...

+ Recent posts