- PVSM.RU - https://www.pvsm.ru -

Создание галереи альбомов

В статье описывается создание функционала синхронизации обложек для альбомов.
Комментарии встречаются как непосредственно в коде, так и в виде пояснений к коммитам.
Будет довольно много кода

Задача:
— сформировать список альбомов
— Найти отсутствующие в системе обложки для каждого альбома
— Сохранить их на устройстве таким образом, чтобы обложки альбомов видел как наш плеер, так и сторонние.

118: создаем дочернее активити, в котором будем отображать загруженные обложки альбомов [1]

— Создаем пустое активити-заглушку
— Добавляем контекстное меню в actionbar, состоящее из «колесика» обновить.
— При создании меню — «колесико начинает крутиться» (заменяется на прогрессбар).
— Определяем кастомный ActionBar background

Далее, я буду оставлять преимущественно ссылки на коммиты, с небольшими пояснениями. Так как иначе статья будет слишком объёмна.

/**
 * Created by recoil on 26.01.14.
 */
public class ActArtworks extends Activity {

    private AQuery aq;
    private Menu optionsMenu;
    private boolean refreshing = true;
    private Activity activity;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //устанавливаем кастомный бэкграунд акшенбара
        getActionBar().setBackgroundDrawable(getResources().getDrawable(R.drawable.ab_bgr));
        //добавляем кнопку назад
        getActionBar().setDisplayHomeAsUpEnabled(true);

        activity = this;
        aq = new AQuery(activity);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        this.optionsMenu = menu;
        //создаем меню в акшенбаре
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.artwork, menu);

        //только после того как меню создано - запускаем обновление
        update();
        return super.onCreateOptionsMenu(menu);
    }

    public void update() {

        AQUtility.debug("Update progress");
        //устанавливаем статус в "обновляется"
        refreshing = true;
        //раскручиваем колесеко
        setRefreshActionButtonState();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                //закрываем активити на нажатие кнопки домой
                finish();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

    public void setRefreshActionButtonState() {

        //если статус обновляется - заменяем иконку обновить на крутящийся прогрессбар
        if (optionsMenu != null) {
            final MenuItem refreshItem = optionsMenu
                    .findItem(R.id.menu_refresh);
            if (refreshItem != null) {
                if (refreshing) {
                    refreshItem.setActionView(R.layout.actionbar_indeterminate_progress);
                } else {
                    refreshItem.setActionView(null);
                }
            }
        }
    }
}

118: создаем класс fillmediastoretracks для считывания всех треков из медиабиблиотеки [2]

— Класс считывает записи из таблицы MediaStore [3] базы данных.
При перезагрузке телефона в системе стартует сервис, осуществляющий сканирование добавленных файлов. Файлы, попадающие под определение «media» добавляются в базу данных.
— По завершении считывания всех данных из таблицы треков — сериализованный ArrayList записывается [4] как бинарный объект.

118: считываем треки [5]

— Сперва пытаемся создать ArrayList всех треков, сериализовав его из сохраненного объекта.
— Если «save»'а — нет, формируем список треков и «сэйв»

118: отображаем список альбомов [6]

— Добавляем GridView в activity
— Добавляем адаптер, для отображения данных
— В адаптере определяем getView, состоящий из единственного текстового поля
— Выводим список всех наименований альбомов, известных андроиду

118: выкидываем дубли альбомов [7]

— Сортируем лист треков по альбому.
— Запускаем итератор, и идем по списку. При дублировании альбома в списке — выкидываем его из массива.

118: добавлена совместимость с андроид 8+ (уф!) [8]

— Добавляем исходный код библиотеки совместимости appcompat [9]
-Переделываем все контекстные меню — добавляем свою схему и в рамках нее определяем как показывать пункт меню

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:FreeAmp="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/menu_refresh"
          android:icon="@drawable/ic_action_refresh"
          android:title=""
          android:alphabeticShortcut="r"
          android:orderInCategory="1"
          FreeAmp:showAsAction="always" />

</menu>

— Наследуемся от встроенного в библиотеку стиля

<style name="theme" parent="@style/Theme.AppCompat">

— Все activity наследуем от ActionBarActivity
— Переопределяем создание менюитемов

-                    refreshItem.setActionView(R.layout.actionbar_indeterminate_progress);
+                    MenuItemCompat.setActionView(refreshItem, R.layout.actionbar_indeterminate_progress);

— Импортируем библиотеку ListPopupWindow — из библиотеки совместимости

Пришлось немножко отвлечься на несколько слабо связанных модификаций (преимущественно баги)
118: нотификейшен бэкграунд [10]
118: ape без тегов обработан. Проверить на других ape [11]
118: добавлена поддержка файлов .opus [12]

119: добавлена библиотека curl [13]

curl for android [14]
libcurl.so size:
(default is ftp, https with ares)
https: ~169K (including http, https)
ares: ~28K (adding to https, with ares support)
ipv6: ~0K (no extra size)
+full: ~278K (all protocols, with ares)
Это достаточно смелое и во многом экспериментальное решение у которого есть как и безусловные достоинства, так и безусловные недостатки. Так как я планирую написать плеер под все популярные платформы — по возможности пробую кроссплатформенные библиотеки. Выйдет ли из curla толк — посмотрим.

Опять небольшое отсупление от основной линии:
119: set ringtone [15]

— Метод для установки текущего трека как рингтон (выдран из сорцов).

119: easy curl request [16]

— Класс обертка для вызова curl. Пока все довольно аскетично. На входе url — на выходе массив байтов (ByteArrayOutputStream). Хочешь — делай из него строка, а не хочешь — делай bitmap. Или еще что ть.

Пришли люди и начали просить сделать поддержку внешних sd cart [17]

121: выкидываем не найденный альбумарт и формируем грид со списком альбомов [18]

— Оборачиваем генерацию списка обложек в асинхронную задачу
121: генерим картинки для гридвью [19]

— Считываем json с обложкой альбома с last.fm через обертку вокруг curl

                     String url = String.format("http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=0cb75104931acd7f44f571ed12cff105&artist=%s&album=%s&format=json", Uri.encode(track.getArtist()),Uri.encode(currentAlbum));
                     getHttpData = new GetHttpData();
                     getHttpData.setUrl(url);
                     getHttpData.request();
                     String result = new String(getHttpData.getByteArray());

— Парсим ответ

JSONObject jsonObject = new JSONObject(result);
                            jsonObject = jsonObject.getJSONObject("album");
                            JSONArray image = jsonObject.getJSONArray("image");
                            for (int i=0;i<image.length();i++) {
                                jsonObject = image.getJSONObject(i);
                                if (jsonObject.getString("size").equals("extralarge")) {
                                    albumArtImageLink = Uri.decode(jsonObject.getString("#text"));

                                    AQUtility.debug(track.getArtist()+":"+currentAlbum,albumArtImageLink);
                                }
                            }

— Загружаем картинку

//download image
                                getHttpData = new GetHttpData();
                                getHttpData.setUrl(albumArtImageLink);
                                getHttpData.request();

                                ContentResolver res = activity.getContentResolver();
                                Bitmap bm = BitmapFactory.decodeByteArray(getHttpData.getByteArray(),0,getHttpData.getByteArray().length);

— Сохраняем ссылку на таблицу (подсмотрено в исходниках)

// Put the newly found artwork in the database.
                                    // Note that this shouldn't be done for the "unknown" album,
                                    // but if this method is called correctly, that won't happen.

                                    // first write it somewhere
                                    String file = Environment.getExternalStorageDirectory()
                                            + "/albumthumbs/" + String.valueOf(System.currentTimeMillis());
                                    if (FileUtils.ensureFileExists(file)) {
                                        try {
                                            OutputStream outstream = new FileOutputStream(file);
                                            if (bm.getConfig() == null) {
                                                bm = bm.copy(Bitmap.Config.RGB_565, false);
                                                if (bm == null) {
                                                    //return getDefaultArtwork(context);
                                                }
                                            }
                                            boolean success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);
                                            outstream.close();
                                            if (success) {
                                                ContentValues values = new ContentValues();
                                                values.put("album_id", track.getAlbumId());
                                                values.put("_data", file);
                                                Uri newuri = res.insert(MediaUtils.sArtworkUri, values);
                                                if (newuri == null) {
                                                    // Failed to insert in to the database. The most likely
                                                    // cause of this is that the item already existed in the
                                                    // database, and the most likely cause of that is that
                                                    // the album was scanned before, but the user deleted the
                                                    // album art from the sd card.
                                                    // We can ignore that case here, since the media provider
                                                    // will regenerate the album art for those entries when
                                                    // it detects this.
                                                    success = false;
                                                }
                                            }
                                            if (!success) {
                                                File f = new File(file);
                                                f.delete();
                                                iterator.remove();
                                            }
                                        } catch (FileNotFoundException e) {
                                            AQUtility.debug( "error creating file", e);
                                        } catch (IOException e) {
                                            AQUtility.debug( "error creating file", e);
                                        }
                                    }

121: out of memory [20]

— Скролим grid вверх вниз и обнаруживаем две вещи:
1. Жуткие лаги (все верно, надо оптимизировать)
2. out of memory (тоже верно, правда словили чуть раньше чем ожидали, так как в сворованный из сорцов код вкрался сюрприз)

121: LRU Cashe [21]

— Создаем выделенную область памяти для хранения изображений, которая будет вытесняться по мере наполнения (http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html)

//Создаем LruCache: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
+    int cacheSize = 20 * 360000; // <7MiB = 300width * 300heigth * 4bytesperpixel * 20images
+    LruCache bitmapCache = new LruCache(cacheSize) {
+        protected int sizeOf(int key, Bitmap value) {
+            return value.getRowBytes() * value.getHeight();//здесь по чесноку считаем
+        }
+    };
+
+    public void addBitmapToMemoryCache(int key, Bitmap bitmap) {
+        synchronized (bitmapCache) {
+            if (getBitmapFromMemCache(key) == null) {
+                bitmapCache.put(key, bitmap);
+            }
+        }
+    }
+

+    public Bitmap getBitmapFromMemCache(int key) {
+        return (Bitmap) bitmapCache.get(key);
+    }

— Считываем ранее загруженные картинки из кеша, добавляем новые картинки в кеш при загрузке

121: загружаем изображения асинхронно [22]

После перехода на вытесняющий кеш произошли две вещи:
1. Пропал out of memory (что логично)
2. Но тормоза при скролле то остались

— Считываем картинки асинхронно

121: onScrollStateChanged, onConfigurationChanged [23]

— Отключаем загрузку картинку, в момент когда пользователь «катнул» скролл и он «парит» по инерции

— Пересоздаем адаптер при изменении ориентации устройства

void applyAdapter() {
        if (tracks == null) return;
        adapter = new AdpArtworks(activity,tracks);
        int iDisplayWidth = getResources().getDisplayMetrics().widthPixels ;
        int numColumns = (iDisplayWidth / 310);
        gridView.setColumnWidth( (iDisplayWidth / numColumns) );
        gridView.setNumColumns(numColumns);
        gridView.setStretchMode( GridView.NO_STRETCH ) ;
        gridView.setAdapter(adapter);
        gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                adapter.setScrollState(scrollState);
                if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
                    adapter.notifyDataSetChanged();
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

            }
        });
    }

121: convert drawable 2 bitmap, update [24]

— Конвертируем xml placeholder'а в изображение
— Вешаем вызов обновления на клик по «колесику» в меню

+        final Drawable imgBgr = activity.getResources().getDrawable(R.drawable.row_bgr);
+        final Bitmap bitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+        imgBgr.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        imgBgr.draw(canvas);
+        this.placeHolder = bitmap;

Автор: recompileme

Источник [25]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/news/54684

Ссылки в тексте:

[1] 118: создаем дочернее активити, в котором будем отображать загруженные обложки альбомов: https://bitbucket.org/recoilme/freeamp/commits/fac00e6f04133172ac9d2bf1bc48ea4c0f80f885

[2] 118: создаем класс fillmediastoretracks для считывания всех треков из медиабиблиотеки: https://bitbucket.org/recoilme/freeamp/commits/9cd35d10fe45376f1f711583b16b88a8fd1f33c7

[3] MediaStore: http://developer.android.com/reference/android/provider/MediaStore.html

[4] записывается: https://bitbucket.org/recoilme/freeamp/src/374969978e81402ac6a06510ff7df488c63071c6/src/ru/recoilme/freeamp/FileUtils.java?at=master

[5] 118: считываем треки: https://bitbucket.org/recoilme/freeamp/commits/ce53da0356def521bc416730ed5f3ee893c8e7f1

[6] 118: отображаем список альбомов: https://bitbucket.org/recoilme/freeamp/commits/0bd00eae2c12e3f160a0934866784c9cb347dccb

[7] 118: выкидываем дубли альбомов: https://bitbucket.org/recoilme/freeamp/commits/b8af45a373bfbca7fb97e6f12e56bccf95b841a0

[8] 118: добавлена совместимость с андроид 8+ (уф!) : https://bitbucket.org/recoilme/freeamp/commits/a56723ed5003a1812702b9c104c7fb19eab4c240

[9] appcompat: http://developer.android.com/tools/support-library/features.html

[10] 118: нотификейшен бэкграунд: https://bitbucket.org/recoilme/freeamp/commits/a7d3401987a790991295319da1d51e883b6c3153

[11] 118: ape без тегов обработан. Проверить на других ape: https://bitbucket.org/recoilme/freeamp/commits/29f75e20b26e6bbff6e998c3bac8bbd6414f5c1f

[12] 118: добавлена поддержка файлов .opus: https://bitbucket.org/recoilme/freeamp/commits/882bfe637cb71122b2b209869827f05e2576f5ac

[13] 119: добавлена библиотека curl: https://bitbucket.org/recoilme/freeamp/commits/9db50a11673eb8849892fd3f7b528cf43f532fcb

[14] curl for android: https://github.com/liudongmiao/curl-android

[15] 119: set ringtone: https://bitbucket.org/recoilme/freeamp/commits/b596520d43670103fc7f3ad3fc4ebac388f26b86

[16] 119: easy curl request: https://bitbucket.org/recoilme/freeamp/commits/e2fdd372b6ae47f71cccdfef78555e7831720090#chg-src/ru/recoilme/freeamp/GetHttpData.java

[17] поддержку внешних sd cart: https://bitbucket.org/recoilme/freeamp/commits/a4a07ac28b39d1efd6f019a771672b261b923313

[18] 121: выкидываем не найденный альбумарт и формируем грид со списком альбомов: https://bitbucket.org/recoilme/freeamp/commits/df18ed3a6087e1317b35576f04c0a519065d25ad

[19] 121: генерим картинки для гридвью: https://bitbucket.org/recoilme/freeamp/commits/5f4179c5a8e077b33bf87745a4aa880438b25291

[20] 121: out of memory: https://bitbucket.org/recoilme/freeamp/commits/905ccd8141664d477fe0fc9db3419c7ffdb58ffb

[21] 121: LRU Cashe: https://bitbucket.org/recoilme/freeamp/commits/8d5a02cc3d4e75ff2e120c33eecbe00be498eb32

[22] 121: загружаем изображения асинхронно: https://bitbucket.org/recoilme/freeamp/commits/8e34322b011d0bac4bea15bbd5a13fa889a4cf6b

[23] 121: onScrollStateChanged, onConfigurationChanged: https://bitbucket.org/recoilme/freeamp/commits/ea697c12e281af14d33f2849a8b35b5405d4912d

[24] 121: convert drawable 2 bitmap, update: https://bitbucket.org/recoilme/freeamp/commits/36e4e86c24bd77a6e49f45bb0c3d4b30b5dc87bb

[25] Источник: http://habrahabr.ru/post/212125/