728x90

Android Audio Player 관련 기능을 테스트 해보고 있는 중이다.

어떤 방식으로 하느냐에 따라 구현할 내용이 달라질 거 같은데 방향 잡기가 쉽지 않다.


먼저 AndroidManifest.xml file 에 권한을 추가한다.

<uses-permission android:name="android.permission.INTERNET" />
<permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />


이미 안드로이드에서는 음악, 주소록같은 기본적인 어플리케이션이 존재한다.
이런 기본적인 앱안에는 정보들을 담은 DB가 구현되어 있다.
이런 앱들은 자신의 앱의 데이터베이스에 접근할 수 있도록 도와주는 컨텐트 프로바이더(Content Provider)란 것을 제공한다.
앱이 ContentProvider를 접근할 때에는 ContentResolver를 이용한다.
ContentResolver는 기본적으로 CRUD(Create Retreive Update Delete) 함수들을 제공하며 이를 통해 다른 어플리케이션에 있는 데이터베이스를 조작할 수 있다.


휴대폰에 있는 오디오 파일을 읽어오는 함수는 아래와 같다.

private void loadAudio() {
    ContentResolver contentResolver = getContentResolver();

    Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
    String selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0";
    String sortOrder = MediaStore.Audio.Media.TITLE + " ASC";
    Cursor cursor = contentResolver.query(uri, null, selection, null, sortOrder);
    cursor.moveToFirst();
    System.out.println("음악파일 개수 = " + cursor.getCount());
    if (cursor != null && cursor.getCount() > 0) {
        do {
            long track_id = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media._ID));
            long albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID));
            String title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
            String album = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
            String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
            long mDuration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
            String datapath = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
            System.out.println("mId = " + track_id + " albumId = " +albumId+ " title : "+title+" album : "+album+" artist: "+artist+" 총시간 : "+mDuration+" data : "+data);
            // Save to audioList
            listViewAdapter.addItem(track_id,albumId,title,artist,album,mDuration,datapath,false);
        } while (cursor.moveToNext());
    }
    cursor.close();
}



listViewAdapter.addItem 을 위한 정의서인 Song_Item 클래스를 만든다.

담을 내용을 정하는 것은 개발자 몫이다.

public class Song_Item {
    private long mId; // 오디오 고유 ID
    private long AlbumId; // 오디오 앨범아트 ID
    private String Title; // 타이틀 정보
    private String Artist; // 아티스트 정보
    private String Album; // 앨범 정보
    private long Duration; // 재생시간
    private String DataPath; // 실제 데이터 위치
    boolean checkBoxState;

    public Song_Item() {
    }

    public Song_Item(long mId, long albumId, String title, String artist, String album, long duration, String dataPath, boolean checkBoxState) {
        this.mId = mId;
        AlbumId = albumId;
        Title = title;
        Artist = artist;
        Album = album;
        Duration = duration;
        DataPath = dataPath;
        this.checkBoxState = checkBoxState;
    }

    public long getmId() {
        return mId;
    }

    public void setmId(long mId) {
        this.mId = mId;
    }

    public long getAlbumId() {
        return AlbumId;
    }

    public void setAlbumId(long albumId) {
        AlbumId = albumId;
    }

    public String getTitle() {
        return Title;
    }

    public void setTitle(String title) {
        Title = title;
    }

    public String getArtist() {
        return Artist;
    }

    public void setArtist(String artist) {
        Artist = artist;
    }

    public String getAlbum() {
        return Album;
    }

    public void setAlbum(String album) {
        Album = album;
    }

    public long getDuration() {
        return Duration;
    }

    public void setDuration(long duration) {
        Duration = duration;
    }

    public String getDataPath() {
        return DataPath;
    }

    public void setDataPath(String dataPath) {
        DataPath = dataPath;
    }

    public boolean isCheckBoxState() {
        return checkBoxState;
    }

    public void setCheckBoxState(boolean checkBoxState) {
        this.checkBoxState = checkBoxState;
    }
}


MainActivity.java 에서 구현할 내용 일부를 발췌하여 적어둔다.

ListViewAdapter 구현 내용이라고 보면 된다.

ListView 상에 가져오는 코드를 작성했는데 Player.java 코드를 아직 제대로 구현하지 못한 상태다.

구글링해서 얻은 코드들을 발췌하고 내가 원하는 기능으로 구현해보는 중이다.

구현하려는 방법이 달라서 기존 코드를 그대로 사용하지는 않았다.

일부 코드는 그대로 활용해도 너무 좋은 코드가 있어서 그냥 사용한 부분도 있다.


ListViewAdapter 에 아무것도 담지 않은 방법으로 먼저 생성한 다음에 loadAudio() 을 읽어들이는 방식은 장점이 있다. loadAudio() 을 전체 오디오 파일을 가져오게 할 수도 있지만 원하는 것만 검색해서 가져오게 하는 방법도 가능하다. 즉 검색기능을 활용하여 데이터를 가져오는 방법이 가능하여 편리하여 이런 방법을 사용하고 있다.

이 부분은 개발자마다 코드 구현 방식이 다른 것을 구글링을 해보면 알 수 있다.


 public class MainActivity extends AppCompatActivity {
    private ListView audiolistView; // 리스트뷰
    private ArrayList<Song_Item> songsList = null; // 데이터 리스트
    private ListViewAdapter listViewAdapter = null; // 리스트뷰에 사용되는 ListViewAdapter

    Context context;

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

        context = this.getBaseContext();

        // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        songsList = new ArrayList<Song_Item>(); // ArrayList 생성
        audiolistView = (ListView) findViewById(R.id.my_listView);
        listViewAdapter = new ListViewAdapter(this); // Adapter 생성
        audiolistView.setAdapter(listViewAdapter); // 어댑터를 리스트뷰에 세팅
        audiolistView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        audiolistView.setOnItemClickListener(songslistener);

        // 체크박스 보임/안보임 관련 코드는 생략했음...

        loadAudio();

    }

    class ViewHolder {
        public ImageView mImgAlbumArt;
        public TextView mTitle;
        public TextView mSubTitle;
        public TextView mDuration;
        public CheckBox checkbox;
    }

    private class ListViewAdapter extends BaseAdapter {
        Context context;

        public ListViewAdapter(Context context) {
            this.context = context;
        }

        // 음악 데이터 추가를 위한 메소드
        public void addItem(long mId, long AlbumId, String Title, String Artist, String Album,long Duration,String DataPath, boolean checkItem_flag) {
            Song_Item item = new Song_Item();
            item.setmId(mId);
            item.setAlbumId(AlbumId);
            item.setTitle(Title);
            item.setArtist(Artist);
            item.setAlbum(Album);
            item.setDuration(Duration);
            item.setDataPath(DataPath);
            item.setCheckBoxState(checkItem_flag);
            songsList.add(item);
        }

        // CheckBox를 모두 선택하는 메서드
        public void setAllChecked(final boolean ischeked) {
            final int tempSize = songsList.size();
            if(isAll == true){
                for (int i = 0; i < tempSize; i++) {
                    songsList.get(i).setCheckBoxState(true);
                }
            } else {
                for (int i = 0; i < tempSize; i++) {
                    songsList.get(i).setCheckBoxState(false);
                }
            }
        }

        @Override
        public int getCount() {
            return songsList.size(); // 데이터 개수 리턴
        }

        @Override
        public Object getItem(int position) {
            return songsList.get(position);
        }

        // 지정한 위치(position)에 있는 데이터 리턴
        @Override
        public long getItemId(int position) {
            return position;
        }

        // position에 위치한 데이터를 화면에 출력하는데 사용될 View를 리턴
        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            final ViewHolder viewHolder;
            final Context context = parent.getContext();
            final Integer index = Integer.valueOf(position);

            // 화면에 표시될 View
            if(convertView == null){
                viewHolder = new ViewHolder();

                LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                convertView = inflater.inflate(R.layout.song_item,parent,false);

                convertView.setBackgroundColor(0x00FFFFFF);
                convertView.invalidate();

                // 화면에 표시될 View 로부터 위젯에 대한 참조 획득
                viewHolder.mImgAlbumArt = (ImageView) convertView.findViewById(R.id.album_Image);
                viewHolder.mTitle = (TextView) convertView.findViewById(R.id.txt_title);
                viewHolder.mSubTitle = (TextView) convertView.findViewById(R.id.txt_subtitle);
                viewHolder.mDuration = (TextView) convertView.findViewById(R.id.txt_duration);
                viewHolder.checkbox = (CheckBox) convertView.findViewById(R.id.list_cell_checkbox);

                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }

            // PersonData 에서 position 에 위치한 데이터 참조 획득
            final Song_Item songItem = songsList.get(position);

            // 아이템 내 각 위젯에 데이터 반영
            Bitmap albumArt = MainActivity.getArtworkQuick(context, (int)songItem.getAlbumId(), 100, 100);
            if(albumArt != null){
                viewHolder.mImgAlbumArt.setImageBitmap(albumArt);
            }
            viewHolder.mTitle.setText(songItem.getTitle());
            viewHolder.mSubTitle.setText(songItem.getArtist());

            int dur = (int) songItem.getDuration();
            int hrs = (dur / 3600000);
            int mns = (dur / 60000) % 60000;
            int scs = dur % 60000 / 1000;
            String songTime = String.format("%02d:%02d:%02d", hrs,  mns, scs);
            if(hrs == 0){
                songTime = String.format("%02d:%02d", mns, scs);
            }
            viewHolder.mDuration.setText(songTime);

            if (isMSG == false) {
                viewHolder.checkbox.setVisibility(View.GONE);
                convertView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Toast.makeText(getApplicationContext(), "노래 재생합니다" + songItem.getAlbumId(), Toast.LENGTH_SHORT).show();

                        Intent intent = new Intent(MainActivity.this, Player.class);
                        intent.putExtra("path", songItem.getDataPath());
                        intent.putExtra("mTitle", songItem.getTitle());
                        intent.putExtra("mDuration", songItem.getDuration());
                        startActivity(intent);
                    }
                });
            } else {
                if (isMSG == true) {
                    viewHolder.checkbox.setVisibility(View.VISIBLE);
                    viewHolder.checkbox.setTag(position); // This line is important.

                    viewHolder.checkbox.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if(songsList.get(position).isCheckBoxState() == true){
                                songsList.get(position).setCheckBoxState(false);
                            } else {
                                songsList.get(position).setCheckBoxState(true);
                            }
                        }
                    });

                    convertView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if(viewHolder.checkbox.isChecked() == false){
                                viewHolder.checkbox.setChecked(true);
                                songsList.get(position).setCheckBoxState(true);
                            } else {
                                viewHolder.checkbox.setChecked(false);
                                songsList.get(position).setCheckBoxState(false);
                            }
                        }
                    });

                }
            }

            // 재사용 문제 해결
            if(songsList.get(position).isCheckBoxState() == true){
                viewHolder.checkbox.setChecked(true);
            } else {
                viewHolder.checkbox.setChecked(false);
            }

            return convertView;
        }

    }

    /* Album ID로 부터 Bitmap 이미지를 생성해 리턴해 주는 메소드 */
    private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
    private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");

    // Get album art for specified album. This method will not try to
    // fall back to getting artwork directly from the file, nor will it attempt to repair the database.
    private static Bitmap getArtworkQuick(Context context, int album_id, int w, int h) {
        w -= 2;
        h -= 2;
        ContentResolver res = context.getContentResolver();
        Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
        if (uri != null) {
            ParcelFileDescriptor fd = null;
            try {
                fd = res.openFileDescriptor(uri, "r");
                int sampleSize = 1;

                sBitmapOptionsCache.inJustDecodeBounds = true;
                BitmapFactory.decodeFileDescriptor(
                        fd.getFileDescriptor(), null, sBitmapOptionsCache);
                int nextWidth = sBitmapOptionsCache.outWidth >> 1;
                int nextHeight = sBitmapOptionsCache.outHeight >> 1;
                while (nextWidth>w && nextHeight>h) {
                    sampleSize <<= 1;
                    nextWidth >>= 1;
                    nextHeight >>= 1;
                }

                sBitmapOptionsCache.inSampleSize = sampleSize;
                sBitmapOptionsCache.inJustDecodeBounds = false;
                Bitmap b = BitmapFactory.decodeFileDescriptor(
                        fd.getFileDescriptor(), null, sBitmapOptionsCache);

                if (b != null) {
                    // finally rescale to exactly the size we need
                    if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
                        Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
                        b.recycle();
                        b = tmp;
                    }
                }

                return b;
            } catch (FileNotFoundException e) {
            } finally {
                try {
                    if (fd != null)
                        fd.close();
                } catch (IOException e) {
                }
            }
        }
        return null;
    }

}


song_item.xml 파일과 activity_main.xml 파일은 첨부파일로 올린다.

layout.zip


이런 형태로 데이터를 가져온 모습을 볼 수 있게 된다.


Player.java 파일 구현할 내용에 따라 넘겨줄 내용이 달라지므로 그 부분까지 완성되면 코드 전체를 첨부해볼 생각이다.

아직은 배워야 할 기능들이 많아서 완성된 코드까지는 많은 시간이 걸릴 것 같다.


두고 두고 읽어보고 싶은 블로그 글을 검색으로 알게되어 적어둔다. 나같은 초보에겐 주옥같은 글이다.

http://unikys.tistory.com/350


앞으로 참고할 사이트

https://www.sitepoint.com/a-step-by-step-guide-to-building-an-android-audio-player-app/


블로그 이미지

Link2Me

,
728x90

Android Studio 상에서 코드 구현 작업을 하던 것을 집과 사무실에서 하다보면 코드를 변경한 것이 어떤 것이 최종인지 헷갈리는 경우가 생긴다.


이럴 경우에는 코드를 육안으로 일일이 비교한다는 건 정말 힘들다.

그렇다고 날짜로만 최신날짜로 덮어쓴다(?)는 것은 정말 위함한 생각이다.


내가 사용하는 방법은 AcroEdit 를 설치하면 포함되어 있는 AcroDiff.exe 파일을 이용한다.


더 좋은 파일 내용비교 프로그램이 있을 수도 있지만, 몇개의 프로그램을 사용해본 결과 이것이 최고로 편하더라.

AcroDiff.exe

첨부된 이 파일만 받아서 이걸 실행하고 파일을 마우스로 Drag & Drop 하여 비교하면 된다.



Total Commander 로 양쪽 파일을 선택하여 마우스로 끌어다가 AcroDiff.exe 파일에 Drop 한다.

화면상에 보면 노란색으로 표시된 부분이 보인다. 명칭을 변경하여 서로 다른 걸 알 수 있다.

달라진 부분을 Android Studio  상에서 Shift + F6 를 눌러서 명칭을 변경해준다.

이렇게 해야만 코드상에서 달라진 명칭을 한번에 모두 변경할 수 있다.

그런 다음 위 그림 1번을 다시 누르면 서로 100% 일치한다고 나오면 양쪽 코드는 동일하다는 결과다.


구글링이나 블로그에서 찾은 코드를 붙여넣기, 프로젝트 읽어오기 등을 하다보면 본인이 원하는 명칭과 다르면 코드 분석이나 작업하는데 불편해서 Android Studio 에서 Shift + F6 으로 명칭을 일괄 변경하면 매우 편하더라.


집과 사무실에서 작업한 코드가 다를 수도 있을 경우에는 이런 방법으로 하면 되지만...

집에서 작업하던 걸 사무실에 가서 작업을 한다고 할 경우에는

src 폴더 밑에 main 폴더만 복사해서 사무실 노트북에 덮어쓰기 또는 사무실 해당 main 폴더를 삭제하고 복사하기를 하면 바로 인식한다.


이렇게 하는 것은 Android Studio 환경이 서로 거의 유사하다는 조건에서 가능하다.

gradle 설정 정보 등은 동일하므로 굳이 Project 내 모든 폴더 파일을 다 옮길 필요가 없더라.

간단하게 핵심이 되는 main 폴더만 복사하면 된다.



블로그 이미지

Link2Me

,
728x90

Android Studio 에서 폴더를 정리하는 방법이다.

코드를 이것 저것 추가하다보니 한 폴더에 너무 많은 내용이 있어서 내용 열람이 깔끔하지 않다.

그래서 새로운 폴더 common 을 윈도우 폴더 만들기에서 생성하고 나서 java 코드를 옮기는 작업을 했다.


옮기고자 하는 파일을 선택한 다음, 마우스로 common 폴더로 Drag & Drop 을 한다.



4번을 누르면.....


파일이 옮겨진 것을 확인할 수 있다.

파일내 package 경로명은 자동으로 변경된다.


마우스 Drag & Drop 으로 옮길 수 있어서 너무 편하고 좋다.


블로그 이미지

Link2Me

,
728x90

Android Studio 에서 모듈 삭제 방법이다.




모듈 삭제해서 화면에서는 안보이지만 실제 폴더에 가면 파일명은 그대로 있다.

즉 연결된 정보만 삭제되었을 뿐이라는 거다.

불필요하면 위 과정후에 실제 파일 경로를 직접 삭제처리 한다.

블로그 이미지

Link2Me

,