5 안드로이드뮤직플레이어 Intent 를활용한 ID3 태그에디터구현안드로이드뮤직플레이어분석 스마트폰에서뮤직플레이어를사용하면서노래제목과가수를안내하는글이깨져보인경험이있는가? 필자는 MP3 의메타정보인 ID3 태그를편집할수있는에디터를만들어이같은사용자의불편을개선하는애플리케이션을배포하고있다. 이번호를통해독자여러분과안드로이드내장뮤직플레이어를분석하고재생중에자동으로음악의정보를찾을수있는기술적인방법을찾아보고자한다. 연재순서 1 회 2011. 1 안드로이드뮤직플레이어분석하기 2 회 2011. 2 안드로이드용 ID3 태그에디터만들기 진성주 moleskine7@gmail.com, http://softwaregeeks.org 호기심왕성하고생활속자동화를실천하려는개발자다. 검색엔진, 대용량분산처리, 성능튜닝에관심이많다. 현재는삼성소프트웨어멤버십 20기, SW 마에스트로 1기회원으로활동하고있다. emarketer 전문리서치기관의통계자료에의하면스마트폰사용자의 90% 정도가스마트폰을미디어이용에사용하고있다. 음악서비스는강력한무기이자킬러애플리케이션으로고객만족도에가장중요한역할을하고있다는것을파악할수있는결과다. < 화면 2> 한글깨짐현상을겪고있는사용자들의불편함 < 화면 1> emarketer 전문리서치기관의통계자료 < 화면 3> 한글깨짐현상을해결하는팁 안드로이드플랫폼기반의뮤직플레이어를정말불편없이사용하고있는가? 많은사용자들이뮤직플레이어에서한글이깨지는현상을겪으며불편함을호소하고있다. 심지어한포털의스마트폰카페에서는한글깨짐을해결하는방법을팁으로올려많은사람들과해결방법을공유하기도한다. 한글깨짐현상의원인뮤직플레이어에서한글깨짐현상이일어나는이유는뭘까? MP3 파일안에는가수, 곡명, 앨범명, 발매일, 장르, 앨범커버등의정보가함께포함돼있는데, 이것을 MP3 ID 태그라고한다. 윈도우의탐색기에서 MP3 파일을선택했을때나타나는음악정 218 m a s o
보가태그다. 태그정보를뮤직플레이어에서활용해곡명과가수정보를출력하는것이다. 안드로이드뮤직플레이어도태그정보를활용한다. 태그정보는문자열로, 글자깨짐현상은글자의인코딩과디코딩의문제로보인다. 따라서원인은다음과같이 2가지로분석할수있다. MP3 파일에서인코딩설정이잘못됨. 안드로이드플랫폼의버그 을사용해미리 PC에서문제를해결할수있다. 즉, PC에서 MP3의정보를정상적으로처리해이용하는것이다. PC에서미리처리해이용하는방법은다수의곡을이용하기에는편하지만 PC를능숙하게다뤄야한다는단점이있다. 또 PC 가꼭필요하다. 그래서안드로이드마켓에서는휴대전화내부에서정보를편집할수있는애플리케이션이사용되기도한다. 태그정보를기록할때문자열의인코딩을바이트값으로설정하는데, 인코딩설정값이실제데이터와달라서글자가깨져보이는것이다. 안드로이드플랫폼에서다국어처리버그가존재하기도한다. 한국뿐아니라중국에서도글자깨짐현상이나타나플랫폼의소스를분석했더니플랫폼의버그로밝혀진바있다. < 화면 6> 안드로이드용태그편집기들 기존애플리케이션의불편함 < 화면 4> 안드로이드플랫폼의다국어처리버그분석글 사용자의 MP3 파일이잘못됐든플랫폼의버그든사용자가불편을겪고있다는것은변하지않는다. 해결방법태그정보의인코딩문제는윈도우와맥을동시에사용하는사용자에게서도나타난다. 운영체제가사용하는기본인코딩이다르기때문이다. 인코딩문제를해결하기위해 PC용애플리케이션이개발돼배포되기도했는데, 사용자는이러한애플리케이션 < 화면 7> 안드로이드마켓의수많은태그편집기들 그럼 PC와안드로이드애플리케이션을사용하면문제가해결되지않을까? 마켓에는 MP3 태그편집기로수십여개의애플리케이션이존재하지만직접편집할곡을선택하고곡의정보를사용자가직접입력해야한다. 여기서우리는 기존뮤직플레이어를사용하면서바로수정할수는없을까? 라며또다른바람을가져본다. 즉, 사용자편의성을높일수있는애플리케이션을만들고싶은것이다. 내장뮤직플레이어에뮬레이터에구동사용자편의성을높이기위해서는다음과같은방법을활용한다. 자동으로태그정보를가져옴. < 화면 5> PC 용태그편집기들 재생중쉽게이동 m a s o 219
실전강의실 안드로이드뮤직플레이어분석 우리는이두가지사항으로편의성을높일것이다. 그렇다면뮤직플레이어가재생중인것을어떻게알수있을까? 또재생중인정보를어떻게알수있을까? 이제부터기술적으로접근해보자. 안드로이드는오픈소스프로젝트다. 즉, 누구나안드로이드소스를볼수있다. 따라서우리는기본뮤직플레이어를다운로드해소스를분석하는것으로어떻게확장할수있는지알아볼것이다. 안드로이드오픈소스프로젝트이동 - http://source.android.com/ index.html Source Browse Source 선택 Git 관리되는소스사이트로이동 - http://android.git.kernel.org/ platform/packages/apps/music.git 선택 다양한버전의소스존재. 안드로이드 2.1 기준으로소스다운로드 heads eclair tree 선택 안드로이드애플리케이션폴더들이존재. [snapshot] 을선택다운로드다음의페이지에서직접다운로드하는것도가능하다. < 화면 9> 내장뮤직플레이어소스폴더구조 리눅스에서 GCC로컴파일하는사람들에게는 Makefile이익숙하지만일반적으로이클립스를사용해안드로이드애플리케이션을제작하는사람들에게는어렵게느껴질수있다. 이클립스를사용해다음과같이프로젝트를설정해보자. FILE NEW Android Application 선택 Create project from existing source 선택 Project name : Music 입력 Location에서다운로드받은프로젝트경로설정 - 여기서는 C:\eclair.tar\Music으로설정했다. Build Target : Android 2.1-update1 선택 OK 버튼클릭 http://android.git.kernel.org/?p=platform/packages/apps/music.g it;a=snapshot;h=refs/heads/eclair;sf=tgz 이클립스프로젝트로불러온후이클립스에서자동으로빌드를시작한다. 하지만정상적으로빌드해실행할수는없다. 왜냐하면내장애플리케이션이라안드로이드 SDK에서제공하지않는안드로이드프레임워크의클래스를사용하기때문이다. 해당애플리케이션이내장플랫폼소스와의존성이있으므로의존성이존재하는부분들을해결해빌드해야한다. 의존성을처리하려면안드로이드기본프레임워크소스가필요하다. 내장애플리케이션을다운받은것처럼안드로이드프레임워크소스도다운받아보자. 안드로이드오픈소스프로젝트이동 - http://source.android.com/ index.html Source Browse Source 선택 Git 관리되는소스사이트로이동 - http://android.git.kernel.org/ platform/frameworks/base.git 선택 다양한버전의소스가존재하는데, 안드로이드 2.1 기준으로소스다 < 화면 8> 내장뮤직플레이어안드로이드오픈소스사이트 다운받고압축을풀면 < 화면 9> 와같은폴더구조를볼수있는데, Makefile로빌드할수있다. 운로드 heads eclair tree 선택 안드로이드플랫폼의폴더들이보인다. snapshot을선택해다운받는다. 다운받은후압축해제후 base 폴더가보이면정상적으로다운로드된것이다. 220 m a s o
다음의페이지에서직접다운로드하는것도가능하다. http://android.git.kernel.org/?p=platform/frameworks/base.git;a= tree;h=refs/heads/eclair;hb=refs/heads/eclair 의존성제거몇단계에걸쳐의존성을제거할것이다. 제거한후에는내장애플리케이션을실행할수있다. 1. android.media.mediafile 의존성제거 - base\media\java\android\media\mediafile.java 파일을 C: \eclair.tar\music\src\android\media 폴더로복사 - SystemProperties 클래스의존성주석처리 < 리스트 1> MediaFile.java 코드의존성제거 C:\eclair.tar\Music\src\android\media\MediaFile.java 20. import android.os.systemproperties; 107. if (SystemProperties.getInt("ro.media.dec.aud.wma.enabled", 0)!= 0) { 108. addfiletype("wma", FILE_TYPE_WMA, "audio/x-mswma"); 109. 129. if (SystemProperties.getInt("ro.media.dec.vid.wmv.enabled", 0)!= 0) { 130. addfiletype("wmv", FILE_TYPE_WMV, "video/x-mswmv"); 131. addfiletype("asf", FILE_TYPE_ASF, "video/x-msasf"); 132. 2. android.r.string.* 의존성제거 - base\core\res\res\values\string.xml 파일을연후 name= fast_ scroll_alphabet, name= fast_scroll_numeric_alphabet 를찾아애플리케이션의 string.xml에복사 < 리스트 2> android.r.string.* 의존성제거 base\core\res\res\values\string.xml <string name="fast_scroll_alphabet">\u0020abcdefghijklmnop QRSTUVWXYZ</string> <string name="fast_scroll_numeric_alphabet"> \u00200123456789abcdefghijklmnopqrstuvwxyz</string> - src\com\android\music\albumbrowseractivity.java 파일 android.r.string.fast_scroll_alphabet R.string.fast_scroll_ alphabet 변경 - src\com\android\music\artistalbumbrowseractivity.java 파일에서 com.android.internal.r.string.fast_scroll_alphabet R.string.fast_scroll_alphabet 변경 - src\com\android\music\musicpicker.java 파일에서 com.android.internal.r.string.fast_scroll_alphabet R.string. fast_scroll_alphabet 변경 - src\com\android\music\trackbrowseractivity.java 파일에서 com.android.internal.r.string.fast_scroll_alphabet R.string. fast_scroll_alphabet 변경 3. android.os.fileutils 의존성제거 C:\eclair.tar\base\core\java\android\os\MediaFile.java 파일을 C:\eclair.tar\Music\src\android\os 폴더로복사 4. android.bluetooth.bluetootha2에의존성제거 MediaButtonIntentReceiver.java 파일에서 import android.blue tooth.bluetootha2에주석처리 5. com.android.internal.database.* 패키지의존성제거 - base\core\java\com\android\internal\database 폴더의 java 파일을 C:\eclair.tar\Music\src\com\android\internal \database 폴더로복사 - 버전차이점으로인한주석처리 < 리스트 3> com.android.internal.database.* 패키지의존성제거 C:\eclair.tar\Music\src\com\android\internal\database \ArrayListCursor.java 116. @Override 117. public boolean deleterow() { 118. return false; 119. C:\eclair.tar\Music\src\com\android\internal\database \SortCursor.java 184. @Override 185. public boolean deleterow() 186. { m a s o 221
실전강의실 안드로이드뮤직플레이어분석 187. return mcursor.deleterow(); 188. 189. 190. @Override 191. public boolean commitupdates() { 192. int length = mcursors.length; 193. for (int i = 0 ; i < length ; i++) { 194. if (mcursors[i]!= null) { 195. mcursors[i].commitupdates(); 196. 197. 198. onchange(true); 199. return true; 200. 8. com.android.internal.widget.verticaltextspinner 의존성제거 - base\core\java\com\android\internal\widget\ VerticalText Spinner.java 파일 C:\eclair.tar\Music\src\com\android \internal\ widget 복사 - 리소스의존성처리 C:\eclair.tar\base\core\res\res\drawable-hdpi\ pickerbox_ background.png, pickerbox_selected.9.png, pickerbox_unselected. 9.png 파일 C:\eclair.tar\Music\res\drawable-hdpi 복사 6. TouchInterceptor 컴파일오류수정 < 리스트 4> TouchInterceptor 의존성제거 C:\eclair.tar\Music\src\com\android\music \TouchInterceptor.java private Context mcontext; 7. 내부이미지의존성제거 - base\core\res\res\drawable-hdpi\ ic_menu_play_clip.png, base\core\res\res\drawable-hdpi\ic_menu_ clear_playlist.png C:\eclair.tar\Music\res\drawable-hdpi 폴더에복사 - com.android.internal.r.drawable.ic_menu_play_clip R.drawable.ic_menu_play_clip com.android.internal.r.drawable.ic_menu_clear_playlist R.drawable.ic_menu_clear_playlist 변경 - TrackBrowserActivity // 추가 public TouchInterceptor(Context context, AttributeSet attrs) { super(context, attrs); mcontext=context; // 추가 SharedPreferences pref = context.getsharedpreferences("music", 3); mremovemode = pref.getint("deletemode", -1); mtouchslop = ViewConfiguration.get(context).getScaledTouchSlop(); Resources res = getresources(); mitemheightnormal = res.getdimensionpixelsize(r.dimen.normal_height); mitemheightexpanded = res.getdimensionpixelsize(r.dimen.expanded_height); com.android.internal.r.drawable.pickerbox_background-> R.drawable.pickerbox_background com.android.internal.r.drawable.pickerbox_selected R.drawable. pickerbox_selected com.android.internal.r.drawable. pickerbox_unselected R.drawable. pickerbox_unselected 컬러값임의변경 context.getresources().getcolor(com.android.internal.r. color.primary_text_light) Color.WHITE context.getresources().getcolor(com.android.internal.r. color.secondary_text_dark) Color.BLACK 상속되지않는코드임의추가 VerticalTextSpinner.java 파일에 private static final int mmeasuredwidth = 100; private static final int mmeasuredheight = 100; 컴파일이잘된다. 하지만실행하면다음과같은메시지를보게된다. Re-installation failed due to different application signatures. You must perform a full uninstall of the application. WARNING: This will remove the application data! 이미내부플레이어가설치돼있기때문에설치가실패됐다는것이다. 에뮬레이터에존재하는내부애플리케이션을제거할수없으므로패키지명을변경해실행한다. 222 m a s o
com.android.music 패키지를 com.android.music.temp로변경 IMediaPlaybackService.aidl 패키지명을 com.android.music.temp 로변경 import com.android.music.imediaplaybackservice; import com.android.music.temp.imediaplaybackservice; 로변경 import com.android.music.r; import com.android.music. temp.r; 로변경 AndroidManifest.xml 파일에서 package="com.android.music " com.android.music.temp로변경패키지를변경하면서패키지의존적인코드를모두변경한것이아니라서완벽하게돌아가는플레이어는아니지만내장플레이어소스를분석할수있는환경은갖춰졌다. 디버그모드로브레이크포인트를찍어독자스스로분석할수있다. 내장뮤직플레이어동작분석이렇게우리는다운받은내장애플리케이션을빌드하고실행도해봤다. 목표는애플리케이션의실행이아니라 뮤직플레이어가재생중인것을어떻게알수있을까 다. 따라서내장뮤직플레이어의핵심부분의동작을분석해봐야한다. 많은안드로이드책에서 Service를설명할때예제로뮤직플레이어를사용한다. 뮤직플레이어는액티비티가종료돼도계속동작해야하기때문에 Service로구현한다는것이다. 지금분석할뮤직플레이어도서비스를이용해실제핵심동작을제어하는데, MediaPlay backservice 파일에서플레이, 일시정지, 멈춤, 다음곡, 이전곡등의기능을수행한다. MediaPlay backactivity에서는버튼으로플레이동작을명령한다. 또한뮤직플레이어는앱위젯도있으므로앱위젯에서도동작명령을내릴수있다. 내장플레이어의핵심동작만표현하면 < 그림 1> 과같다. < 그림 1> 뮤직플레이어핵심동작 < 그림 1> 을보면중간에 IMediaPlaybackService.aidl과 Broadcast Intent가있다. 이두가지방식이뮤직플레이어의서비스와통신하는방법이다. 이방법을하나씩알아보자. AIDL(Android Interface Definition Language) 안드로이드는리눅스커널위에올라간플랫폼이다. 따라서멀티프로세스로동작하며내부프로세스간에통신할수있는 IPC(Interprocess Communication) 를사용해다른프로세스와통신할수있다. AIDL은내부통신을할수있도록인터페이스정의를한것이다. 즉, 애플리케이션내부에서다른프로세스에접근해동작을제어해야한다면 AIDL 파일로인터페이스를정의해통신을진행하는것이다. < 리스트 5> IMediaPlaybackService.aidl 파일 Music\src\com\android\music\IMediaPlaybackService.aidl package com.android.music.temp; import android.graphics.bitmap; interface IMediaPlaybackService { void openfile(string path, boolean oneshot); void openfileasync(string path); void open(in long [] list, intposition); intgetqueueposition(); boolean isplaying(); void stop(); void pause(); void play(); void prev(); void next(); long duration(); long position(); long seek(long pos); String gettrackname(); String getalbumname(); long getalbumid(); String getartistname(); long getartistid(); void enqueue(in long [] list, intaction); long [] getqueue(); void movequeueitem(intfrom,intto); void setqueueposition(intindex); String getpath(); long getaudioid(); void setshufflemode(intshufflemode); intgetshufflemode(); intremovetracks(intfirst,intlast); intremovetrack(longid); void setrepeatmode(intrepeatmode); intgetrepeatmode(); intgetmediamountedcount(); m a s o 223
실전강의실 안드로이드뮤직플레이어분석 IMediaPlaybackService.aidl은뮤직플레이어의기능을모두정의하고있다. 문법은자바의인터페이스와비슷하지만자바인터페이스는아니다. 저장된파일이.aidl임을잊지말자. Broadcast Intent 앱위젯은 AppWidgetProvider를확장해내부동작을처리한다. 내부에서 View를처리할때 View에 setonclickpending Intent를등록해클릭시이벤트를처리한다. 따라서앱위젯에서는 AIDL로직접서비스에접근해원하는동작을처리할수없다. 앱위젯에서는이벤트처리시기본적으로 Broadcast하기때문에 PendingIntent를등록해 Intent 정보로처리한다. < 리스트 6> MediaAppWidgetProvider 핵심코드 Music\src\com\android\music\MediaAppWidgetProvider.java 중략 private void linkbuttons(context context, RemoteViews views, boolean playeractive) { // Connect up various buttons and touch events Intent intent; PendingIntent pendingintent; final ComponentName servicename = new ComponentName(context, MediaPlaybackService.class); if (playeractive) { intent = new Intent(context, MediaPlaybackActivityStarter.class); pendingintent = PendingIntent.getActivity(context, 0 /* no requestcode */, intent, 0 /* no flags */); views.setonclickpendingintent(r.id.album_appwidget, pendingintent); else { intent = new Intent(context, MusicBrowserActivity.class); pendingintent = PendingIntent.getActivity(context, 0 /* no requestcode */, intent, 0 /* no flags */); views.setonclickpendingintent(r.id.album_appwidget, pendingintent); intent = new Intent(MediaPlaybackService.TOGGLEPAUSE_ ACTION); intent.setcomponent(servicename); pendingintent = PendingIntent.getService(context, 0 /* no requestcode */, intent, 0 /* no flags */); views.setonclickpendingintent(r.id.control_play, pendingintent); intent = new Intent(MediaPlaybackService.NEXT_ACTION); intent.setcomponent(servicename); pendingintent = PendingIntent.getService(context, 0 /* no requestcode */, intent, 0 /* no flags */); views.setonclickpendingintent(r.id.control_next, pendingintent); 중략 MediaPlaybackService와 MediaPlaybackActivity 통신방법인 AIDL이 MediaAppWidgetProvider와 Broadcast된 Intent 로동작을처리한다는것을알았다. 이제까지액티비티에서서비스를호출하는법을생각해봤는데, 서비스에서액티비티로접근할필요성도있다. 예를들면 A라는곡이종료되고다음곡으로이동하는것을액티비티와앱위젯쪽으로알려줘야한다. Intent 를 Broadcast해이를처리한다. < 리스트 7> MediaPlaybackService 핵심코드 Music\src\com\android\music\MediaPlaybackService.java 중략 private void notifychange(string what) { Intent i = new Intent(what); i.putextra("id", Long.valueOf(getAudioId())); i.putextra("artist", getartistname()); i.putextra("album",getalbumname()); i.putextra("track", gettrackname()); sendbroadcast(i); if (what.equals(queue_changed)) { savequeue(true); else { savequeue(false); // Share this notification directly with our widgets mappwidgetprovider.notifychange(this, what); 중략 MediaPlaybackService 핵심코드는 < 리스트 7> 과같다. 이를통해재생시정보를얻어올수있다. 내장뮤직플레이어음악정보가져오기 MediaPlaybackService에서상태가변경되면항상 Board cast로정보를알려주는것을확인할수있다. 안드로이드프로젝트를만들어 BroadcastReceiver를작성해실제로음악정보를확인한다. < 리스트 8> MediaIntentReceiver 코드 MediaIntentReceiver.java 224 m a s o
package org.softwaregeeks; import android.content.broadcastreceiver; import android.content.context; import android.content.intent; import android.widget.toast; public class MediaIntentReceiver extends BroadcastReceiver { public static final int NOTIFICATION_ID = 300000; @Override public void onreceive(context context, Intent intent) { String intentaction = intent.getaction(); if( intentaction.startswith("com.android.music")) { Long id = (long) intent.getlongextra("id",0l); String track = intent.getstringextra("track"); String artist = intent.getstringextra("artist"); String album = intent.getstringextra("album"); Long albumid = (long) intent.getlongextra("albumid",0l); android:name="com.android.music.playbackcomplete" /> android:name="com.android.music.asyncopencomplete" /> android:name="com.android.music.musicservicecommand.pause" /> </intent-filter> </receiver> </application> <uses-sdk android:minsdkversion="7" /> </manifest> 뮤직플레이어의소스를확인해음악정보를가져올수있음을알았고 BroadcastReceiver를작성해내장뮤직플레이어를실행하면음악정보가토스트로나타나는것도볼수있다. String message = "["+id+"]" + track + " by " + artist; Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); < 리스트 9> AndroidManifest.xml 코드 AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android. com/apk/res/android" package="org.softwaregeeks" android:versioncode="1" android:versionname="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <receiverandroid:name="org.softwaregeeks.mediaintentreceiv er"> <intent-filter> <!-- Base --> android:name="com.android.music.playstatechanged" /> android:name="com.android.music.metachanged" /> android:name="com.android.music.queuechanged" /> < 화면 10> 내장뮤직플레이어에서음악정보를얻어출력한화면 안드로이드는개발자의호기심을채워줄수있는오픈소스프로젝트다. 음악정보를가져올수있다는기술적인사실로재밌는애플리케이션도만들수있다. 예를들면곡의재생횟수를기록해음악을추천하는애플리케이션등의개발이가능한것이다. 다음호에서는내장플레이어가재생중일때자동으로연결하는법과음악의메타데이터를편집할수있는 ID3 태그에디터를만들어보자. 이달의디스켓 : ID3 태그에디터.zip m a s o 225