poniedziałek, 28 marca 2016

[DSP2016] Android prosto z poligonu odc.4 (MediaStore, uprawnienia, AsyncTask)

Witam ponownie, tym razem rano w tzw. lany poniedziałek. Mogę już wczytywać pliki muzyczne z telefonu oraz rozpocząłem prace nad odtwarzaniem ich w tle.

Jak odczytywać pliki muzyczne z całego urządzenia?  Służy do tego MediaStore (content provider w stylu lokalnej bazy SQLite) ze źródłem w internal lub external storage. U mnie w FileListActivity wyszukiwanie plików audio wygląda następująco:

private void loadAudioFiles(String searchText) {
try {

mModel = new ArrayList<MediaFileItem>();

String[] projection = new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media.MIME_TYPE
};

String selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0";
if (!TextUtils.isEmpty(searchText))
selection += " AND " + MediaStore.Audio.Media.TITLE + " LIKE '%" + searchText + "%'";

String sortOrder = MediaStore.Audio.Media.DATE_ADDED + " DESC";

Cursor cursor = getContentResolver().query(MediaStore.Audio.Media.INTERNAL_CONTENT_URI,projection,selection,null,sortOrder);

if (cursor != null && cursor.moveToFirst()) {
do {
int idColumn = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
int artistColumn = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
int titleColumn = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
int durationColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
int filePathIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
int mimeTypeColumn = cursor.getColumnIndex (MediaStore.Audio.Media.MIME_TYPE);

MediaFileItem audio = new MediaFileItem(
cursor.getLong(idColumn),
cursor.getString(titleColumn),
cursor.getString(artistColumn),
cursor.getInt(durationColumn),
cursor.getString(filePathIndex),
cursor.getString(mimeTypeColumn));

mModel.add(audio);

} while (cursor.moveToNext());
}

} catch (Exception e) {
e.printStackTrace();
}
}
W oficjalnej dokumentacji temat nie wydaje się dogłębnie objaśniony, warto jednak rzucić okiem na koniec strony http://developer.android.com/guide/topics/media/mediaplayer.html. Do multimediów w Android mogę polecić też książkę.
Aby skorzystać z external storage (w Android external nie musi znajdować się hm … na karcie SD, może być też w pamięci wbudowanej w urządzenie) należy zadeklarować uprawnienie READ_EXTERNAL_STORAGE.  Do czasów Android 6 to wystarczało, ale od 6-tki runtime uprawnień uległ mocnej przebudowie i teraz trzeba sprawdzać uprawnienia w trakcie wykonywania. Sprawę bardzo dobrze opisuje artykuł http://inthecheesefactory.com/blog/things-you-need-to-know-about-android-m-permission-developer-edition/en.  Kierując się zawartymi w nim wskazówkami spłodziłem kilka linijek, które działają jak powinny:
private void searchFiles() {

int hasReadExternalStoragePermission = ActivityCompat.checkSelfPermission(FileListActivity.this,Manifest.permission.READ_EXTERNAL_STORAGE);

if (hasReadExternalStoragePermission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(FileListActivity.this,
new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}

useExternalStorage = true;
doSearchFiles();
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_PERMISSIONS:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
useExternalStorage = true;
} else {
useExternalStorage = false;
Toast.makeText(FileListActivity.this, "READ_EXTERNAL_STORAGE Denied", Toast.LENGTH_SHORT)
.show();
}

doSearchFiles();

break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
W polskojęzycznej wersji Android 6 zdziwiła mnie nazwa uprawnienia w ustawieniach przy uprawnieniach rozważanej tutaj aplikacji. Otóż nazywa się … “Pamięć”. W pierwszym momencie nie byłem pewny czy to jest tym czym myślę, ale wyłączenie switcha spowodowało wyskoczenie komunikatu z pytaniem o nadanie uprawnienia przy kolejnym najbliższym wyszukiwaniu –Winking smile
Patrząc na powyższe listingi można zadać pytanie, co znajduje się pomiędzy doSearchFiles a loadAudioFiles.  Mamy tam wywołanie asynchronicznego taska. Generalnie chodzi o to, żeby dość czasochłonna operacja jaką jest przeszukiwanie MediaStore nie przyblokowywała nam interfejsu użytkownika. Aby tak się nie działo, należy uruchomić ją w innym wątku. Mamy jednak drugą dekadę XXI wieku, więc nie będziemy tego czynić od podstaw ręcznie. W C# do takich rzeczy może służyć async na poziomie języka (Xamarin pozwala na używanie async), my jedziemy tutaj na Javie i posłuży nam do tego nieco mniej uniwersalny i mniej transparentny odpowiednik tego bytu a mianowicie AsyncTask. Można sobie poczytać o nim choćby na stronie http://developer.android.com/reference/android/os/AsyncTask.html. Ma trochę ograniczeń (np. co do typów), trzeba się troszkę więcej opisać, ale nie jest najgorzej, powstaje dość zrozumiały kod. Na http://www.androiddevelopersolutions.com/2016/01/android-recyclerview-and-cardview.html mamy całkiem niezły artykuł pokazujący jak można wykorzystać AsyncTask do odświeżania adaptera RecyclerView. U mnie task przybrał ostatecznie postać:
public class MediaAsyncTask extends AsyncTask<String, Void, Boolean> {

@Override
protected void onPreExecute() {
//setProgressBarIndeterminateVisibility(true);
}

@Override
protected Boolean doInBackground(String... params) {
Boolean result = false;
String searchText = params[0];
try {
loadAudioFiles(searchText);
result = true;

} catch (Exception e) {
e.printStackTrace();
result = false;
}

return result;
}

@Override
protected void onPostExecute(Boolean result) {

//setProgressBarIndeterminateVisibility(false);

if (result) {
mAdapter.setFilter(mModel);
}
}
}
Wywołuję go sobie w metodzie doSearchFiles:
private void doSearchFiles() {
new MediaAsyncTask().execute(searchText);
}
Metodę SearchFiles wywołuję w onCreate po ustawieniu adaptera oraz przy zmienie frazy tekstowej w SearchView. 
Po uzyskaniu dostępu do zawartości muzycznej urządzenia naturalnym krokiem jest implementacja odtwarzania. Generalnie służy do tego klasa MediaPlayer, którą możemy sobie osadzić bezpośrednio w aktywności lub przenieść do serwisu, by cieszyć się odtwarzaniem audio w tle. Implentacja takiego serwisu może być dość sporym zagadnieniem. W następnym odcinku naświetlimy tę sprawę.

Brak komentarzy: