From b27ce42d02664c50c2f77c253480f0395e125648 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sun, 24 Jun 2018 19:35:14 +0200 Subject: [PATCH 1/9] Extract ImageLoader interface. This will allow to swap it with a new implementation. Signed-off-by: Yahor Berdnikau --- .../activity/SubsonicTabActivity.java | 44 +- .../moire/ultrasonic/util/ImageLoader.java | 471 +----------------- .../ultrasonic/util/LegacyImageLoader.java | 450 +++++++++++++++++ .../org/moire/ultrasonic/view/AlbumView.java | 1 - .../moire/ultrasonic/view/ChatAdapter.java | 1 - .../moire/ultrasonic/view/EntryAdapter.java | 1 - 6 files changed, 482 insertions(+), 486 deletions(-) create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java index 46b73a67..a92f6e41 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -20,7 +20,6 @@ package org.moire.ultrasonic.activity; import android.app.AlertDialog; import android.app.Dialog; -import android.app.NotificationManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -33,54 +32,23 @@ import android.os.Bundle; import android.os.Environment; import android.support.v7.app.ActionBar; import android.util.Log; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; +import android.view.*; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.RemoteViews; -import android.widget.TextView; - +import android.widget.*; import net.simonvt.menudrawer.MenuDrawer; import net.simonvt.menudrawer.Position; - import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.domain.Share; -import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.DownloadService; -import org.moire.ultrasonic.service.DownloadServiceImpl; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import org.moire.ultrasonic.util.BackgroundTask; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator; -import org.moire.ultrasonic.util.ImageLoader; -import org.moire.ultrasonic.util.ModalBackgroundTask; -import org.moire.ultrasonic.util.ShareDetails; -import org.moire.ultrasonic.util.SilentBackgroundTask; -import org.moire.ultrasonic.util.TabActivityBackgroundTask; -import org.moire.ultrasonic.util.TimeSpan; -import org.moire.ultrasonic.util.TimeSpanPicker; -import org.moire.ultrasonic.util.Util; -import org.moire.ultrasonic.util.VideoPlayerType; +import org.moire.ultrasonic.service.*; +import org.moire.ultrasonic.util.*; import java.io.File; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import java.util.regex.Pattern; /** @@ -829,7 +797,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen { if (IMAGE_LOADER == null || !IMAGE_LOADER.isRunning()) { - IMAGE_LOADER = new ImageLoader(this, Util.getImageLoaderConcurrency(this)); + IMAGE_LOADER = new LegacyImageLoader(this, Util.getImageLoaderConcurrency(this)); IMAGE_LOADER.startImageLoader(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java index 60fe1b49..74202148 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java @@ -1,462 +1,43 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ package org.moire.ultrasonic.util; -import android.content.Context; -import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.TransitionDrawable; -import android.os.Handler; -import android.text.TextUtils; -import android.util.Log; import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicBoolean; +public interface ImageLoader { + boolean isRunning(); -/** - * Asynchronous loading of images, with caching. - *

- * There should normally be only one instance of this class. - * - * @author Sindre Mehus - */ -public class ImageLoader implements Runnable -{ - private static final String TAG = ImageLoader.class.getSimpleName(); + void setConcurrency(int concurrency); - private final LRUCache cache = new LRUCache(150); - private final BlockingQueue queue; - private int imageSizeDefault; - private final int imageSizeLarge; - private Bitmap largeUnknownImage; - private Bitmap unknownAvatarImage; - private Context context; - private Collection threads; - private AtomicBoolean running = new AtomicBoolean(); - private int concurrency; + void startImageLoader(); - public ImageLoader(Context context, int concurrency) - { - this.context = context; - this.concurrency = concurrency; - queue = new LinkedBlockingQueue(1000); + void stopImageLoader(); - Resources resources = context.getResources(); - Drawable drawable = resources.getDrawable(R.drawable.unknown_album); + void loadAvatarImage( + View view, + String username, + boolean large, + int size, + boolean crossFade, + boolean highQuality + ); - // Determine the density-dependent image sizes. - if (drawable != null) - { - imageSizeDefault = drawable.getIntrinsicHeight(); - } + void loadImage( + View view, + MusicDirectory.Entry entry, + boolean large, + int size, + boolean crossFade, + boolean highQuality + ); - imageSizeLarge = Util.getMaxDisplayMetric(context); - createLargeUnknownImage(context); - createUnknownAvatarImage(context); - } + Bitmap getImageBitmap(String username, int size); - public synchronized boolean isRunning() - { - return running.get() && !threads.isEmpty(); - } + Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size); - public void setConcurrency(int concurrency) - { - this.concurrency = concurrency; - } + void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size); - public void startImageLoader() - { - running.set(true); + void addImageToCache(Bitmap bitmap, String username, int size); - threads = Collections.synchronizedCollection(new ArrayList(this.concurrency)); - - for (int i = 0; i < this.concurrency; i++) - { - Thread thread = new Thread(this, String.format("ImageLoader_%d", i)); - threads.add(thread); - thread.start(); - } - } - - public synchronized void stopImageLoader() - { - clear(); - - for (Thread thread : threads) - { - thread.interrupt(); - } - - running.set(false); - threads.clear(); - } - - private void createLargeUnknownImage(Context context) - { - BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large); - Log.i(TAG, "createLargeUnknownImage"); - - if (drawable != null) - { - largeUnknownImage = Util.scaleBitmap(drawable.getBitmap(), imageSizeLarge); - } - } - - private void createUnknownAvatarImage(Context context) - { - Resources res = context.getResources(); - Drawable contact = res.getDrawable(R.drawable.ic_contact_picture); - unknownAvatarImage = Util.createBitmapFromDrawable(contact); - } - - public void loadAvatarImage(View view, String username, boolean large, int size, boolean crossFade, boolean highQuality) - { - view.invalidate(); - - if (username == null) - { - setUnknownAvatarImage(view); - return; - } - - if (size <= 0) - { - size = large ? imageSizeLarge : imageSizeDefault; - } - - Bitmap bitmap = cache.get(getKey(username, size)); - - if (bitmap != null) - { - setAvatarImageBitmap(view, username, bitmap, crossFade); - return; - } - - setUnknownAvatarImage(view); - - queue.offer(new Task(view, username, size, large, crossFade, highQuality)); - } - - public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, boolean crossFade, boolean highQuality) - { - view.invalidate(); - - if (entry == null) - { - setUnknownImage(view, large); - return; - } - - String coverArt = entry.getCoverArt(); - - if (TextUtils.isEmpty(coverArt)) { - setUnknownImage(view, large); - return; - } - - if (size <= 0) - { - size = large ? imageSizeLarge : imageSizeDefault; - } - - Bitmap bitmap = cache.get(getKey(coverArt, size)); - - if (bitmap != null) - { - setImageBitmap(view, entry, bitmap, crossFade); - return; - } - - setUnknownImage(view, large); - - queue.offer(new Task(view, entry, size, large, crossFade, highQuality)); - } - - private static String getKey(String coverArtId, int size) - { - return String.format("%s:%d", coverArtId, size); - } - - public Bitmap getImageBitmap(String username, int size) - { - Bitmap bitmap = cache.get(getKey(username, size)); - - if (bitmap != null && !bitmap.isRecycled()) - { - Bitmap.Config config = bitmap.getConfig(); - return bitmap.copy(config, false); - } - - return null; - } - - public Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size) - { - if (entry == null) - { - return null; - } - - String coverArt = entry.getCoverArt(); - - if (TextUtils.isEmpty(coverArt)) { - return null; - } - - if (size <= 0) - { - size = large ? imageSizeLarge : imageSizeDefault; - } - - Bitmap bitmap = cache.get(getKey(coverArt, size)); - - if (bitmap != null && !bitmap.isRecycled()) - { - Bitmap.Config config = bitmap.getConfig(); - return bitmap.copy(config, false); - } - - return null; - } - - private void setImageBitmap(View view, MusicDirectory.Entry entry, Bitmap bitmap, boolean crossFade) - { - if (view instanceof ImageView) - { - ImageView imageView = (ImageView) view; - - MusicDirectory.Entry tagEntry = (MusicDirectory.Entry) view.getTag(); - - // Only apply image to the view if the view is intended for this entry - if (entry != null && tagEntry != null && !entry.equals(tagEntry)) - { - Log.i(TAG, "View is no longer valid, not setting ImageBitmap"); - return; - } - - if (crossFade) - { - Drawable existingDrawable = imageView.getDrawable(); - Drawable newDrawable = Util.createDrawableFromBitmap(this.context, bitmap); - - if (existingDrawable == null) - { - Bitmap emptyImage = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); - existingDrawable = new BitmapDrawable(context.getResources(), emptyImage); - } - - Drawable[] layers = new Drawable[]{existingDrawable, newDrawable}; - - TransitionDrawable transitionDrawable = new TransitionDrawable(layers); - imageView.setImageDrawable(transitionDrawable); - transitionDrawable.startTransition(250); - } - else - { - imageView.setImageBitmap(bitmap); - } - } - } - - private void setAvatarImageBitmap(View view, String username, Bitmap bitmap, boolean crossFade) - { - if (view instanceof ImageView) - { - ImageView imageView = (ImageView) view; - - String tagEntry = (String) view.getTag(); - - // Only apply image to the view if the view is intended for this entry - if (username != null && tagEntry != null && !username.equals(tagEntry)) - { - Log.i(TAG, "View is no longer valid, not setting ImageBitmap"); - return; - } - - if (crossFade) - { - Drawable existingDrawable = imageView.getDrawable(); - Drawable newDrawable = Util.createDrawableFromBitmap(this.context, bitmap); - - if (existingDrawable == null) - { - Bitmap emptyImage = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); - existingDrawable = new BitmapDrawable(context.getResources(), emptyImage); - } - - Drawable[] layers = new Drawable[]{existingDrawable, newDrawable}; - - TransitionDrawable transitionDrawable = new TransitionDrawable(layers); - imageView.setImageDrawable(transitionDrawable); - transitionDrawable.startTransition(250); - } - else - { - imageView.setImageBitmap(bitmap); - } - } - } - - public void setUnknownAvatarImage(View view) - { - setAvatarImageBitmap(view, null, unknownAvatarImage, false); - } - - public void setUnknownImage(View view, boolean large) - { - if (large) - { - setImageBitmap(view, null, largeUnknownImage, false); - } - else - { - if (view instanceof TextView) - { - ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0); - } - else if (view instanceof ImageView) - { - ((ImageView) view).setImageResource(R.drawable.unknown_album); - } - } - } - - public void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size) - { - cache.put(getKey(entry.getCoverArt(), size), bitmap); - } - - public void addImageToCache(Bitmap bitmap, String username, int size) - { - cache.put(getKey(username, size), bitmap); - } - - public void clear() - { - queue.clear(); - } - - @Override - public void run() - { - while (running.get()) - { - try - { - Task task = queue.take(); - task.execute(); - } - catch (InterruptedException ignored) - { - running.set(false); - break; - } - catch (Throwable x) - { - Log.e(TAG, "Unexpected exception in ImageLoader.", x); - } - } - } - - private class Task - { - private final View view; - private final MusicDirectory.Entry entry; - private final String username; - private final Handler handler; - private final int size; - private final boolean saveToFile; - private final boolean crossFade; - private final boolean highQuality; - - public Task(View view, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean crossFade, boolean highQuality) - { - this.view = view; - this.entry = entry; - this.username = null; - this.size = size; - this.saveToFile = saveToFile; - this.crossFade = crossFade; - this.highQuality = highQuality; - handler = new Handler(); - } - - public Task(View view, String username, int size, boolean saveToFile, boolean crossFade, boolean highQuality) - { - this.view = view; - this.entry = null; - this.username = username; - this.size = size; - this.saveToFile = saveToFile; - this.crossFade = crossFade; - this.highQuality = highQuality; - handler = new Handler(); - } - - public void execute() - { - try - { - MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); - final boolean isAvatar = this.username != null && this.entry == null; - final Bitmap bitmap = this.entry != null ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); - - if (isAvatar) - addImageToCache(bitmap, username, size); - else - addImageToCache(bitmap, entry, size); - - handler.post(new Runnable() - { - @Override - public void run() - { - if (isAvatar) - { - setAvatarImageBitmap(view, username, bitmap, crossFade); - } - else - { - setImageBitmap(view, entry, bitmap, crossFade); - } - } - }); - } - catch (Throwable x) - { - Log.e(TAG, "Failed to download album art.", x); - } - } - } + void clear(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java new file mode 100644 index 00000000..ae1fc517 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java @@ -0,0 +1,450 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package org.moire.ultrasonic.util; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import org.moire.ultrasonic.R; +import org.moire.ultrasonic.domain.MusicDirectory; +import org.moire.ultrasonic.service.MusicService; +import org.moire.ultrasonic.service.MusicServiceFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Asynchronous loading of images, with caching. + *

+ * There should normally be only one instance of this class. + * + * @author Sindre Mehus + */ +public class LegacyImageLoader implements Runnable, ImageLoader { + private static final String TAG = LegacyImageLoader.class.getSimpleName(); + + private final LRUCache cache = new LRUCache<>(150); + private final BlockingQueue queue; + private int imageSizeDefault; + private final int imageSizeLarge; + private Bitmap largeUnknownImage; + private Bitmap unknownAvatarImage; + private Context context; + private Collection threads; + private AtomicBoolean running = new AtomicBoolean(); + private int concurrency; + + public LegacyImageLoader( + Context context, + int concurrency + ) { + this.context = context; + this.concurrency = concurrency; + queue = new LinkedBlockingQueue<>(1000); + + Resources resources = context.getResources(); + Drawable drawable = resources.getDrawable(R.drawable.unknown_album); + + // Determine the density-dependent image sizes. + if (drawable != null) { + imageSizeDefault = drawable.getIntrinsicHeight(); + } + + imageSizeLarge = Util.getMaxDisplayMetric(context); + createLargeUnknownImage(context); + createUnknownAvatarImage(context); + } + + @Override + public synchronized boolean isRunning() { + return running.get() && !threads.isEmpty(); + } + + @Override + public void setConcurrency(int concurrency) { + this.concurrency = concurrency; + } + + @Override + public void startImageLoader() { + running.set(true); + + threads = Collections.synchronizedCollection(new ArrayList(this.concurrency)); + + for (int i = 0; i < this.concurrency; i++) { + Thread thread = new Thread(this, String.format("ImageLoader_%d", i)); + threads.add(thread); + thread.start(); + } + } + + @Override + public synchronized void stopImageLoader() { + clear(); + + for (Thread thread : threads) { + thread.interrupt(); + } + + running.set(false); + threads.clear(); + } + + private void createLargeUnknownImage(Context context) { + BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large); + Log.i(TAG, "createLargeUnknownImage"); + + if (drawable != null) { + largeUnknownImage = Util.scaleBitmap(drawable.getBitmap(), imageSizeLarge); + } + } + + private void createUnknownAvatarImage(Context context) { + Resources res = context.getResources(); + Drawable contact = res.getDrawable(R.drawable.ic_contact_picture); + unknownAvatarImage = Util.createBitmapFromDrawable(contact); + } + + @Override + public void loadAvatarImage( + View view, + String username, + boolean large, + int size, + boolean crossFade, + boolean highQuality + ) { + view.invalidate(); + + if (username == null) { + setUnknownAvatarImage(view); + return; + } + + if (size <= 0) { + size = large ? imageSizeLarge : imageSizeDefault; + } + + Bitmap bitmap = cache.get(getKey(username, size)); + + if (bitmap != null) { + setAvatarImageBitmap(view, username, bitmap, crossFade); + return; + } + + setUnknownAvatarImage(view); + + queue.offer(new Task(view, username, size, large, crossFade, highQuality)); + } + + @Override + public void loadImage( + View view, + MusicDirectory.Entry entry, + boolean large, + int size, + boolean crossFade, + boolean highQuality + ) { + view.invalidate(); + + if (entry == null) { + setUnknownImage(view, large); + return; + } + + String coverArt = entry.getCoverArt(); + + if (TextUtils.isEmpty(coverArt)) { + setUnknownImage(view, large); + return; + } + + if (size <= 0) { + size = large ? imageSizeLarge : imageSizeDefault; + } + + Bitmap bitmap = cache.get(getKey(coverArt, size)); + + if (bitmap != null) { + setImageBitmap(view, entry, bitmap, crossFade); + return; + } + + setUnknownImage(view, large); + + queue.offer(new Task(view, entry, size, large, crossFade, highQuality)); + } + + private static String getKey(String coverArtId, int size) { + return String.format("%s:%d", coverArtId, size); + } + + @Override + public Bitmap getImageBitmap(String username, int size) { + Bitmap bitmap = cache.get(getKey(username, size)); + + if (bitmap != null && !bitmap.isRecycled()) { + Bitmap.Config config = bitmap.getConfig(); + return bitmap.copy(config, false); + } + + return null; + } + + @Override + public Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size) { + if (entry == null) { + return null; + } + + String coverArt = entry.getCoverArt(); + + if (TextUtils.isEmpty(coverArt)) { + return null; + } + + if (size <= 0) { + size = large ? imageSizeLarge : imageSizeDefault; + } + + Bitmap bitmap = cache.get(getKey(coverArt, size)); + + if (bitmap != null && !bitmap.isRecycled()) { + Bitmap.Config config = bitmap.getConfig(); + return bitmap.copy(config, false); + } + + return null; + } + + private void setImageBitmap( + View view, + MusicDirectory.Entry entry, + Bitmap bitmap, + boolean crossFade + ) { + if (view instanceof ImageView) { + ImageView imageView = (ImageView) view; + + MusicDirectory.Entry tagEntry = (MusicDirectory.Entry) view.getTag(); + + // Only apply image to the view if the view is intended for this entry + if (entry != null && tagEntry != null && !entry.equals(tagEntry)) { + Log.i(TAG, "View is no longer valid, not setting ImageBitmap"); + return; + } + + if (crossFade) { + Drawable existingDrawable = imageView.getDrawable(); + Drawable newDrawable = Util.createDrawableFromBitmap(this.context, bitmap); + + if (existingDrawable == null) { + Bitmap emptyImage = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + existingDrawable = new BitmapDrawable(context.getResources(), emptyImage); + } + + Drawable[] layers = new Drawable[]{existingDrawable, newDrawable}; + + TransitionDrawable transitionDrawable = new TransitionDrawable(layers); + imageView.setImageDrawable(transitionDrawable); + transitionDrawable.startTransition(250); + } else { + imageView.setImageBitmap(bitmap); + } + } + } + + private void setAvatarImageBitmap( + View view, + String username, + Bitmap bitmap, + boolean crossFade + ) { + if (view instanceof ImageView) { + ImageView imageView = (ImageView) view; + + String tagEntry = (String) view.getTag(); + + // Only apply image to the view if the view is intended for this entry + if (username != null && + tagEntry != null && + !username.equals(tagEntry)) { + Log.i(TAG, "View is no longer valid, not setting ImageBitmap"); + return; + } + + if (crossFade) { + Drawable existingDrawable = imageView.getDrawable(); + Drawable newDrawable = Util.createDrawableFromBitmap(this.context, bitmap); + + if (existingDrawable == null) { + Bitmap emptyImage = Bitmap.createBitmap( + bitmap.getWidth(), + bitmap.getHeight(), + Bitmap.Config.ARGB_8888 + ); + existingDrawable = new BitmapDrawable(context.getResources(), emptyImage); + } + + Drawable[] layers = new Drawable[]{existingDrawable, newDrawable}; + + TransitionDrawable transitionDrawable = new TransitionDrawable(layers); + imageView.setImageDrawable(transitionDrawable); + transitionDrawable.startTransition(250); + } else { + imageView.setImageBitmap(bitmap); + } + } + } + + private void setUnknownAvatarImage(View view) { + setAvatarImageBitmap(view, null, unknownAvatarImage, false); + } + + private void setUnknownImage(View view, boolean large) { + if (large) { + setImageBitmap(view, null, largeUnknownImage, false); + } else { + if (view instanceof TextView) { + ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0); + } else if (view instanceof ImageView) { + ((ImageView) view).setImageResource(R.drawable.unknown_album); + } + } + } + + @Override + public void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size) { + cache.put(getKey(entry.getCoverArt(), size), bitmap); + } + + @Override + public void addImageToCache(Bitmap bitmap, String username, int size) { + cache.put(getKey(username, size), bitmap); + } + + @Override + public void clear() { + queue.clear(); + } + + @Override + public void run() { + while (running.get()) { + try { + Task task = queue.take(); + task.execute(); + } catch (InterruptedException ignored) { + running.set(false); + break; + } catch (Throwable x) { + Log.e(TAG, "Unexpected exception in ImageLoader.", x); + } + } + } + + private class Task { + private final View view; + private final MusicDirectory.Entry entry; + private final String username; + private final Handler handler; + private final int size; + private final boolean saveToFile; + private final boolean crossFade; + private final boolean highQuality; + + Task( + View view, + MusicDirectory.Entry entry, + int size, + boolean saveToFile, + boolean crossFade, + boolean highQuality + ) { + this.view = view; + this.entry = entry; + this.username = null; + this.size = size; + this.saveToFile = saveToFile; + this.crossFade = crossFade; + this.highQuality = highQuality; + handler = new Handler(); + } + + Task( + View view, + String username, + int size, + boolean saveToFile, + boolean crossFade, + boolean highQuality + ) { + this.view = view; + this.entry = null; + this.username = username; + this.size = size; + this.saveToFile = saveToFile; + this.crossFade = crossFade; + this.highQuality = highQuality; + handler = new Handler(); + } + + public void execute() { + try { + MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); + final boolean isAvatar = this.username != null && this.entry == null; + final Bitmap bitmap = this.entry != null + ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) + : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); + + if (isAvatar) + addImageToCache(bitmap, username, size); + else + addImageToCache(bitmap, entry, size); + + handler.post(new Runnable() { + @Override + public void run() { + if (isAvatar) { + setAvatarImageBitmap(view, username, bitmap, crossFade); + } else { + setImageBitmap(view, entry, bitmap, crossFade); + } + } + }); + } catch (Throwable x) { + Log.e(TAG, "Failed to download album art.", x); + } + } + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java index 23eb0074..1884e201 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java @@ -25,7 +25,6 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; - import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.service.MusicService; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java index 9fc000c2..0361e81f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java @@ -8,7 +8,6 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; - import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.domain.ChatMessage; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java index 4b3b5317..ce229d65 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java @@ -24,7 +24,6 @@ import android.widget.ArrayAdapter; import android.widget.CheckedTextView; import android.widget.ImageView; import android.widget.TextView; - import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.util.ImageLoader; From e4e962faa06051c5a2acd5cb71800ed05c43ccd6 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Tue, 26 Jun 2018 21:10:37 +0200 Subject: [PATCH 2/9] Changed android namespace from a to android. Signed-off-by: Yahor Berdnikau --- ultrasonic/src/main/AndroidManifest.xml | 248 ++++++++++++------------ 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 11730690..bcf7f624 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -1,205 +1,205 @@ - + android:installLocation="auto"> - - - - - - - + + + + + + + + android:anyDensity="true" + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" + android:xlargeScreens="true"/> + android:allowBackup="false" + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/Theme.AppCompat" + android:name=".app.UApp" + android:label="@string/common.appname"> + android:name=".activity.MainActivity" + android:configChanges="orientation|keyboardHidden" + android:label="UltraSonic" + android:launchMode="standard"> - - + + + android:name=".activity.SelectArtistActivity" + android:configChanges="orientation|keyboardHidden" + android:launchMode="standard"/> + android:name=".activity.SelectAlbumActivity" + android:configChanges="orientation|keyboardHidden"/> + android:name=".activity.SearchActivity" + android:configChanges="orientation|keyboardHidden" + android:label="@string/search.label" + android:launchMode="singleTask"/> + android:name=".activity.SelectPlaylistActivity" + android:configChanges="orientation|keyboardHidden" + android:label="@string/playlist.label" + android:launchMode="standard"/> + android:name=".activity.PodcastsActivity" + android:configChanges="orientation|keyboardHidden" + android:label="@string/podcasts.label" + android:launchMode="standard"/> + android:name=".activity.BookmarkActivity" + android:configChanges="orientation|keyboardHidden"/> + android:name=".activity.ShareActivity" + android:configChanges="orientation|keyboardHidden"/> + android:name=".activity.ChatActivity" + android:configChanges="orientation|keyboardHidden"/> + android:name=".activity.DownloadActivity" + android:configChanges="keyboardHidden" + android:launchMode="singleTask" + android:exported="true" /> + android:name=".activity.SettingsActivity" + android:configChanges="orientation|keyboardHidden" + android:launchMode="singleTask"/> + android:name=".activity.HelpActivity" + android:configChanges="orientation|keyboardHidden" + android:launchMode="singleTask"/> + android:name=".activity.LyricsActivity" + android:configChanges="orientation|keyboardHidden" + android:launchMode="singleTask"/> + android:name=".activity.EqualizerActivity" + android:configChanges="orientation|keyboardHidden" + android:label="@string/equalizer.label" + android:launchMode="singleTask"/> + android:name=".activity.SelectGenreActivity" + android:configChanges="orientation|keyboardHidden" + android:launchMode="standard"/> + android:name=".activity.VoiceQueryReceiverActivity" + android:launchMode="singleTask"> - - + + + android:name=".activity.QueryReceiverActivity" + android:launchMode="singleTask"> - - + + + android:name="android.app.searchable" + android:resource="@xml/searchable"/> - + + android:name=".service.DownloadServiceImpl" + android:label="UltraSonic Download Service" + android:exported="false"> - - - - - - + + + + + + - - - + + + - + - - - - + + + + + android:name=".provider.UltraSonicAppWidgetProvider4x1" + android:label="UltraSonic (4x1)"> - + + android:name="android.appwidget.provider" + android:resource="@xml/appwidget_info_4x1"/> + android:name=".provider.UltraSonicAppWidgetProvider4x2" + android:label="UltraSonic (4x2)"> - + + android:name="android.appwidget.provider" + android:resource="@xml/appwidget_info_4x2"/> + android:name=".provider.UltraSonicAppWidgetProvider4x3" + android:label="UltraSonic (4x3)"> - + + android:name="android.appwidget.provider" + android:resource="@xml/appwidget_info_4x3"/> + android:name=".provider.UltraSonicAppWidgetProvider4x4" + android:label="UltraSonic (4x4)"> - + + android:name="android.appwidget.provider" + android:resource="@xml/appwidget_info_4x4"/> + android:name=".provider.SearchSuggestionProvider" + android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/> + android:name="android.app.default_searchable" + android:value="org.moire.ultrasonic.activity.QueryReceiverActivity"/> + android:name=".receiver.A2dpIntentReceiver" + android:exported="false"> - + From 74591571bfe07bc8ee22533f55531288e32b673f Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Tue, 26 Jun 2018 21:11:39 +0200 Subject: [PATCH 3/9] Add initial implementation of image loader. Currently it only supports loading cover art images from network. Signed-off-by: Yahor Berdnikau --- dependencies.gradle | 10 +- settings.gradle | 1 + subsonic-api-image-loader/build.gradle | 66 ++++++++++++++ .../subsonic/loader/image/CommonFunctions.kt | 9 ++ .../image/CoverArtRequestHandlerTest.kt | 86 ++++++++++++++++++ .../loader/image/RequestCreatorTest.kt | 18 ++++ .../resources/Big_Buck_Bunny.jpeg | Bin 0 -> 10452 bytes .../src/main/AndroidManifest.xml | 4 + .../loader/image/CoverArtRequestHandler.kt | 32 +++++++ .../subsonic/loader/image/RequestCreator.kt | 14 +++ .../loader/image/SubsonicImageLoader.kt | 20 ++++ ultrasonic/build.gradle | 1 + 12 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 subsonic-api-image-loader/build.gradle create mode 100644 subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt create mode 100644 subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt create mode 100644 subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt create mode 100644 subsonic-api-image-loader/src/integrationTest/resources/Big_Buck_Bunny.jpeg create mode 100644 subsonic-api-image-loader/src/main/AndroidManifest.xml create mode 100644 subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt create mode 100644 subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt create mode 100644 subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt diff --git a/dependencies.gradle b/dependencies.gradle index 437b99a1..4282a4f8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -21,12 +21,15 @@ ext.versions = [ semver : "1.0.0", twitterSerial : "0.1.6", koin : "0.9.3", + picasso : "2.71828", junit : "4.12", mockito : "2.16.0", mockitoKotlin : "1.5.0", kluent : "1.35", apacheCodecs : "1.10", + testRunner : "1.0.1", + robolectric : "3.8", ] ext.gradlePlugins = [ @@ -40,6 +43,7 @@ ext.gradlePlugins = [ ext.androidSupport = [ support : "com.android.support:support-v4:$versions.androidSupport", design : "com.android.support:design:$versions.androidSupport", + annotations : "com.android.support:support-annotations:$versions.androidSupport" ] ext.other = [ @@ -53,7 +57,8 @@ ext.other = [ semver : "net.swiftzer.semver:semver:$versions.semver", twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial", koinCore : "org.koin:koin-core:$versions.koin", - koinAndroid : "org.koin:koin-android:$versions.koin" + koinAndroid : "org.koin:koin-android:$versions.koin", + picasso : "com.squareup.picasso:picasso:$versions.picasso", ] ext.testing = [ @@ -63,6 +68,9 @@ ext.testing = [ mockito : "org.mockito:mockito-core:$versions.mockito", mockitoInline : "org.mockito:mockito-inline:$versions.mockito", kluent : "org.amshove.kluent:kluent:$versions.kluent", + kluentAndroid : "org.amshove.kluent:kluent-android:$versions.kluent", mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp", apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs", + testRunner : "com.android.support.test:runner:$versions.testRunner", + robolectric : "org.robolectric:robolectric:$versions.robolectric", ] diff --git a/settings.gradle b/settings.gradle index 256032c5..eb222b3d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ include ':library' include ':domain' include ':subsonic-api' +include ':subsonic-api-image-loader' include ':cache' include ':menudrawer' include ':pulltorefresh' diff --git a/subsonic-api-image-loader/build.gradle b/subsonic-api-image-loader/build.gradle new file mode 100644 index 00000000..7fc23d69 --- /dev/null +++ b/subsonic-api-image-loader/build.gradle @@ -0,0 +1,66 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +//apply plugin: 'jacoco' +apply from: '../gradle_scripts/code_quality.gradle' + +android { + compileSdkVersion(versions.compileSdk) + + defaultConfig { + minSdkVersion(versions.minSdk) + targetSdkVersion(versions.targetSdk) + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + test.java.srcDirs += "${projectDir}/src/integrationTest/kotlin" + test.resources.srcDirs += "${projectDir}/src/integrationTest/resources" + } +} + +dependencies { + api project(':domain') + api project(':subsonic-api') + api other.kotlinStdlib + api other.picasso + + testImplementation testing.junit + testImplementation testing.kotlinJunit + testImplementation testing.mockito + testImplementation testing.mockitoInline + testImplementation testing.mockitoKotlin + testImplementation testing.kluent + testImplementation testing.robolectric +} + +jacoco { + toolVersion(versions.jacoco) +} + +//ext { +// jacocoExclude = [] +//} + +//jacocoTestReport { +// reports { +// html.enabled true +// csv.enabled false +// xml.enabled true +// } +// +// afterEvaluate { +// classDirectories = files(classDirectories.files.collect { +// fileTree(dir: it, excludes: jacocoExclude) +// }) +// } +//} +// +//test.finalizedBy jacocoTestReport +//test { +// jacoco { +// excludes += jacocoExclude +// } +//} diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt new file mode 100644 index 00000000..5c4be754 --- /dev/null +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt @@ -0,0 +1,9 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import okio.Okio +import java.io.InputStream + +fun Any.loadResourceStream(name: String): InputStream { + val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name))) + return source.inputStream() +} diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt new file mode 100644 index 00000000..a4d74733 --- /dev/null +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt @@ -0,0 +1,86 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import android.net.Uri +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.anyOrNull +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import com.squareup.picasso.Picasso +import com.squareup.picasso.Request +import org.amshove.kluent.`should equal` +import org.amshove.kluent.`should not be` +import org.amshove.kluent.`should throw` +import org.amshove.kluent.shouldEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.robolectric.RobolectricTestRunner +import java.io.IOException + +@RunWith(RobolectricTestRunner::class) +class CoverArtRequestHandlerTest { + private val mockSubsonicApiClientMock = mock() + private val handler = CoverArtRequestHandler(mockSubsonicApiClientMock) + + @Test + fun `Should accept only cover art request`() { + val requestUri = createLoadCoverArtRequest("some-id") + + handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo true + } + + @Test + fun `Should not accept random request uri`() { + val requestUri = Uri.Builder() + .scheme(SCHEME) + .authority(AUTHORITY) + .appendPath("random") + .build() + + handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo false + } + + @Test + fun `Should fail loading if uri doesn't contain id`() { + var requestUri = createLoadCoverArtRequest("some-id") + requestUri = requestUri.buildUpon().clearQuery().build() + + val fail = { + handler.load(requestUri.buildRequest(), 0) + } + + fail `should throw` IllegalStateException::class + } + + @Test + fun `Should throw IOException when request to api failed`() { + val streamResponse = StreamResponse(null, null, 500) + whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull())) + .thenReturn(streamResponse) + + val fail = { + handler.load(createLoadCoverArtRequest("some").buildRequest(), 0) + } + + fail `should throw` IOException::class + } + + @Test + fun `Should load bitmap from network`() { + val streamResponse = StreamResponse( + loadResourceStream("Big_Buck_Bunny.jpeg"), + apiError = null, + responseHttpCode = 200 + ) + whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull())) + .thenReturn(streamResponse) + + val response = handler.load(createLoadCoverArtRequest("some").buildRequest(), 0) + + response.loadedFrom `should equal` Picasso.LoadedFrom.NETWORK + response.source `should not be` null + } + + private fun Uri.buildRequest() = Request.Builder(this).build() +} diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt new file mode 100644 index 00000000..09927850 --- /dev/null +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt @@ -0,0 +1,18 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import android.net.Uri +import org.amshove.kluent.shouldEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RequestCreatorTest { + @Test + fun `Should create valid load cover art request`() { + val entityId = "299" + val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$COVER_ART_PATH?id=$entityId") + + createLoadCoverArtRequest(entityId).compareTo(expectedUri).shouldEqualTo(0) + } +} diff --git a/subsonic-api-image-loader/src/integrationTest/resources/Big_Buck_Bunny.jpeg b/subsonic-api-image-loader/src/integrationTest/resources/Big_Buck_Bunny.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..758dfa64294976541eb73221aa5a6d43b6c456c2 GIT binary patch literal 10452 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 + + diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt new file mode 100644 index 00000000..7d898437 --- /dev/null +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt @@ -0,0 +1,32 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import com.squareup.picasso.Picasso.LoadedFrom.NETWORK +import com.squareup.picasso.Request +import com.squareup.picasso.RequestHandler +import okio.Okio +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import java.io.IOException + +/** + * Loads cover arts from subsonic api. + */ +class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : RequestHandler() { + override fun canHandleRequest(data: Request): Boolean { + return with(data.uri) { + scheme == SCHEME && + authority == AUTHORITY && + path == "/$COVER_ART_PATH" + } + } + + override fun load(request: Request, networkPolicy: Int): Result { + val id = request.uri.getQueryParameter("id") + + val response = apiClient.getCoverArt(id) + if (response.hasError()) { + throw IOException("${response.apiError}") + } else { + return Result(Okio.source(response.stream), NETWORK) + } + } +} diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt new file mode 100644 index 00000000..eb300fe5 --- /dev/null +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt @@ -0,0 +1,14 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import android.net.Uri + +internal const val SCHEME = "subsonic_api" +internal const val AUTHORITY = BuildConfig.APPLICATION_ID +internal const val COVER_ART_PATH = "cover_art" + +internal fun createLoadCoverArtRequest(entityId: String): Uri = Uri.Builder() + .scheme(SCHEME) + .authority(AUTHORITY) + .appendPath(COVER_ART_PATH) + .appendQueryParameter("id", entityId) + .build() diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt new file mode 100644 index 00000000..6a3225b4 --- /dev/null +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt @@ -0,0 +1,20 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import android.content.Context +import android.widget.ImageView +import com.squareup.picasso.Picasso +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient + +class SubsonicImageLoader( + private val context: Context, + apiClient: SubsonicAPIClient +) { + private val picasso = Picasso.Builder(context) + .addRequestHandler(CoverArtRequestHandler(apiClient)) + .build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) } + + fun loadCoverArt(entityId: String, view: ImageView) { + picasso.load(createLoadCoverArtRequest(entityId)) + .into(view) + } +} diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index bf224674..c2d49505 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -55,6 +55,7 @@ dependencies { implementation project(':library') implementation project(':domain') implementation project(':subsonic-api') + implementation project(':subsonic-api-image-loader') implementation project(':cache') implementation androidSupport.support From 77eb257d84d28dff2ac7d3b45d2723bb82a99775 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Tue, 26 Jun 2018 21:38:28 +0200 Subject: [PATCH 4/9] Glue together old ImageLoader impl and new one. Currently it is working in a pretty limited way. Signed-off-by: Yahor Berdnikau --- .../activity/SubsonicTabActivity.java | 26 ++++++++----- .../kotlin/org/moire/ultrasonic/app/UApp.kt | 11 +++++- .../moire/ultrasonic/di/MusicServiceModule.kt | 9 ++++- .../subsonic/SubsonicImageLoaderProxy.kt | 39 +++++++++++++++++++ 4 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java index a92f6e41..cd7fcf46 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -39,11 +39,13 @@ import android.widget.*; import net.simonvt.menudrawer.MenuDrawer; import net.simonvt.menudrawer.Position; import org.moire.ultrasonic.R; +import org.moire.ultrasonic.app.UApp; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.domain.Share; import org.moire.ultrasonic.service.*; +import org.moire.ultrasonic.subsonic.SubsonicImageLoaderProxy; import org.moire.ultrasonic.util.*; import java.io.File; @@ -793,16 +795,22 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen if (IMAGE_LOADER != null && IMAGE_LOADER.isRunning()) IMAGE_LOADER.clear(); } - public synchronized ImageLoader getImageLoader() - { - if (IMAGE_LOADER == null || !IMAGE_LOADER.isRunning()) - { - IMAGE_LOADER = new LegacyImageLoader(this, Util.getImageLoaderConcurrency(this)); - IMAGE_LOADER.startImageLoader(); - } + public synchronized ImageLoader getImageLoader() { + if (IMAGE_LOADER == null || + !IMAGE_LOADER.isRunning()) { + LegacyImageLoader legacyImageLoader = new LegacyImageLoader( + this, + Util.getImageLoaderConcurrency(this) + ); + IMAGE_LOADER = new SubsonicImageLoaderProxy( + legacyImageLoader, + ((UApp) getApplication()).getSubsonicImageLoader() + ); + IMAGE_LOADER.startImageLoader(); + } - return IMAGE_LOADER; - } + return IMAGE_LOADER; + } void download(final boolean append, final boolean save, final boolean autoPlay, final boolean playNext, final boolean shuffle, final List songs) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index f8bb9d24..97fcc8bb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -1,10 +1,12 @@ package org.moire.ultrasonic.app import android.app.Application +import org.koin.android.ext.android.get import org.koin.android.ext.android.startKoin import org.moire.ultrasonic.di.baseNetworkModule import org.moire.ultrasonic.di.directoriesModule import org.moire.ultrasonic.di.musicServiceModule +import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader import org.moire.ultrasonic.util.Util class UApp : Application() { @@ -15,7 +17,14 @@ class UApp : Application() { startKoin(this, listOf( directoriesModule, baseNetworkModule, - musicServiceModule(sharedPreferences) + musicServiceModule(sharedPreferences, this) )) } + + /** + * Temporary method to get subsonic image loader from java code. + */ + fun getSubsonicImageLoader(): SubsonicImageLoader { + return get() + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 884b4988..7809ff1b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -1,6 +1,7 @@ @file:JvmName("MusicServiceModule") package org.moire.ultrasonic.di +import android.content.Context import android.content.SharedPreferences import android.util.Log import org.koin.dsl.module.applicationContext @@ -14,6 +15,7 @@ import org.moire.ultrasonic.service.CachedMusicService import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.OfflineMusicService import org.moire.ultrasonic.service.RESTMusicService +import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader import org.moire.ultrasonic.util.Constants import kotlin.math.abs @@ -24,7 +26,10 @@ private const val DEFAULT_SERVER_INSTANCE = 1 private const val UNKNOWN_SERVER_URL = "not-exists" private const val LOG_TAG = "MusicServiceModule" -fun musicServiceModule(sp: SharedPreferences) = applicationContext { +fun musicServiceModule( + sp: SharedPreferences, + context: Context +) = applicationContext { context(MUSIC_SERVICE_CONTEXT) { subsonicApiModule() @@ -109,5 +114,7 @@ fun musicServiceModule(sp: SharedPreferences) = applicationContext { bean(name = OFFLINE_MUSIC_SERVICE) { return@bean OfflineMusicService(get(), get()) } + + bean { return@bean SubsonicImageLoader(context, get()) } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt new file mode 100644 index 00000000..5412cf53 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt @@ -0,0 +1,39 @@ +package org.moire.ultrasonic.subsonic + +import android.view.View +import android.widget.ImageView +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader +import org.moire.ultrasonic.util.ImageLoader +import org.moire.ultrasonic.util.LegacyImageLoader + +/** + * Temporary proxy between new [SubsonicImageLoader] and [ImageLoader] interface and old + * [LegacyImageLoader] implementation. + * + * Should be removed on [LegacyImageLoader] removal. + */ +class SubsonicImageLoaderProxy( + legacyImageLoader: LegacyImageLoader, + private val subsonicImageLoader: SubsonicImageLoader +) : ImageLoader by legacyImageLoader { + override fun loadImage( + view: View?, + entry: MusicDirectory.Entry?, + large: Boolean, + size: Int, + crossFade: Boolean, + highQuality: Boolean + ) { + val id = entry?.coverArt + + if (id != null && + view != null && + view is ImageView) { + subsonicImageLoader.loadCoverArt( + entityId = id, + view = view + ) + } + } +} From 6f6e2470d89206730f3e2c2218cdd68df90750ee Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Tue, 26 Jun 2018 21:54:26 +0200 Subject: [PATCH 5/9] Enable jacoco coverage for subsonic-api-image-loader module. Signed-off-by: Yahor Berdnikau --- gradle_scripts/jacoco.gradle | 13 ++++++++-- subsonic-api-image-loader/build.gradle | 35 ++++++++------------------ 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/gradle_scripts/jacoco.gradle b/gradle_scripts/jacoco.gradle index 5be89860..c8209068 100644 --- a/gradle_scripts/jacoco.gradle +++ b/gradle_scripts/jacoco.gradle @@ -21,6 +21,7 @@ def createJacocoFullReportTask() { description = "Generate full Jacoco coverage report including all modules." def subsonicApi = project.findProject("subsonic-api") + def subsonicApiImageLoader = project.findProject("subsonic-api-image-loader") def ultrasonicApp = project.findProject("ultrasonic") def cache = project.findProject("cache") @@ -29,6 +30,10 @@ def createJacocoFullReportTask() { dir: "${subsonicApi.buildDir}/classes/main", excludes: subsonicApi.jacocoExclude ), + fileTree( + dir: "${subsonicApiImageLoader.buildDir}/intermediates/classes/debug/org", + excludes: subsonicApiImageLoader.jacocoExclude + ), fileTree( dir: "${ultrasonicApp.buildDir}/intermediates/classes/debug/org", excludes: ultrasonicApp.jacocoExclude @@ -38,8 +43,12 @@ def createJacocoFullReportTask() { excludes: cache.jacocoExclude ) ) - sourceDirectories = files(subsonicApi.sourceSets.main.getAllSource(), - ultrasonicApp.extensions.getByName('android').sourceSets.main.java.sourceFiles) + sourceDirectories = files( + subsonicApi.sourceSets.main.getAllSource(), + subsonicApiImageLoader.extensions.getByName('android').sourceSets.main.java.sourceFiles, + ultrasonicApp.extensions.getByName('android').sourceSets.main.java.sourceFiles, + cache.sourceSets.main.getAllSource(), + ) executionData = files("${buildDir}/jacoco/jacoco.exec") reports { diff --git a/subsonic-api-image-loader/build.gradle b/subsonic-api-image-loader/build.gradle index 7fc23d69..cd7ead54 100644 --- a/subsonic-api-image-loader/build.gradle +++ b/subsonic-api-image-loader/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -//apply plugin: 'jacoco' +apply plugin: 'jacoco-android' apply from: '../gradle_scripts/code_quality.gradle' android { @@ -40,27 +40,14 @@ jacoco { toolVersion(versions.jacoco) } -//ext { -// jacocoExclude = [] -//} +ext { + jacocoExclude = [] +} -//jacocoTestReport { -// reports { -// html.enabled true -// csv.enabled false -// xml.enabled true -// } -// -// afterEvaluate { -// classDirectories = files(classDirectories.files.collect { -// fileTree(dir: it, excludes: jacocoExclude) -// }) -// } -//} -// -//test.finalizedBy jacocoTestReport -//test { -// jacoco { -// excludes += jacocoExclude -// } -//} +jacocoAndroidUnitTestReport { + excludes += jacocoExclude +} + +afterEvaluate { + testDebugUnitTest.finalizedBy jacocoTestDebugUnitTestReport +} \ No newline at end of file From c23420a83a8d712ea3d075a03426ddba2297794c Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Tue, 26 Jun 2018 22:15:45 +0200 Subject: [PATCH 6/9] Fix lint error by excluding support library from picasso dependency. Signed-off-by: Yahor Berdnikau --- subsonic-api-image-loader/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/subsonic-api-image-loader/build.gradle b/subsonic-api-image-loader/build.gradle index cd7ead54..7f7d3a71 100644 --- a/subsonic-api-image-loader/build.gradle +++ b/subsonic-api-image-loader/build.gradle @@ -25,7 +25,9 @@ dependencies { api project(':domain') api project(':subsonic-api') api other.kotlinStdlib - api other.picasso + api(other.picasso) { + exclude group: "com.android.support" + } testImplementation testing.junit testImplementation testing.kotlinJunit From c0b6500b47174581cc7628458fae7d226f355393 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Thu, 28 Jun 2018 22:03:47 +0200 Subject: [PATCH 7/9] Provide configuration for image load request. Signed-off-by: Yahor Berdnikau --- .../loader/image/SubsonicImageLoader.kt | 39 +++++++++++++++++-- .../subsonic/SubsonicImageLoaderProxy.kt | 11 ++++-- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt index 6a3225b4..0f73ae2f 100644 --- a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt @@ -6,15 +6,46 @@ import com.squareup.picasso.Picasso import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient class SubsonicImageLoader( - private val context: Context, + context: Context, apiClient: SubsonicAPIClient ) { private val picasso = Picasso.Builder(context) .addRequestHandler(CoverArtRequestHandler(apiClient)) .build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) } - fun loadCoverArt(entityId: String, view: ImageView) { - picasso.load(createLoadCoverArtRequest(entityId)) - .into(view) + fun load(request: ImageRequest) = when (request) { + is ImageRequest.CoverArt -> loadCoverArt(request) + } + + private fun loadCoverArt(request: ImageRequest.CoverArt) { + picasso.load(createLoadCoverArtRequest(request.entityId)) + .apply { + if (request.placeHolderDrawableRes != null) { + placeholder(request.placeHolderDrawableRes) + } + } + .apply { + if (request.errorDrawableRes != null) { + error(request.errorDrawableRes) + } + } + .into(request.imageView) } } + +sealed class ImageRequest( + val placeHolderDrawableRes: Int? = null, + val errorDrawableRes: Int? = null, + val imageView: ImageView +) { + class CoverArt( + val entityId: String, + imageView: ImageView, + placeHolderDrawableRes: Int? = null, + errorDrawableRes: Int? = null + ) : ImageRequest( + placeHolderDrawableRes, + errorDrawableRes, + imageView + ) +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt index 5412cf53..83fb4d83 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt @@ -2,7 +2,9 @@ package org.moire.ultrasonic.subsonic import android.view.View import android.widget.ImageView +import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.subsonic.loader.image.ImageRequest import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader import org.moire.ultrasonic.util.ImageLoader import org.moire.ultrasonic.util.LegacyImageLoader @@ -30,10 +32,13 @@ class SubsonicImageLoaderProxy( if (id != null && view != null && view is ImageView) { - subsonicImageLoader.loadCoverArt( - entityId = id, - view = view + val request = ImageRequest.CoverArt( + id, + view, + placeHolderDrawableRes = R.drawable.unknown_album, + errorDrawableRes = R.drawable.unknown_album ) + subsonicImageLoader.load(request) } } } From 02467cb05beb4aab40a8cc773720344a44b41f7d Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sat, 14 Jul 2018 20:55:45 +0200 Subject: [PATCH 8/9] Add loading user avatars. Signed-off-by: Yahor Berdnikau --- .../loader/image/AvatarRequestHandlerTest.kt | 73 +++++++++++++++++++ .../loader/image/RequestCreatorTest.kt | 10 ++- .../loader/image/AvatarRequestHandler.kt | 34 +++++++++ .../loader/image/CoverArtRequestHandler.kt | 2 +- .../subsonic/loader/image/RequestCreator.kt | 12 ++- .../loader/image/SubsonicImageLoader.kt | 53 +++++++++++--- .../subsonic/SubsonicImageLoaderProxy.kt | 21 ++++++ 7 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt create mode 100644 subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt new file mode 100644 index 00000000..4dbc5f33 --- /dev/null +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt @@ -0,0 +1,73 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import android.net.Uri +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import com.squareup.picasso.Picasso +import com.squareup.picasso.Request +import org.amshove.kluent.`should equal` +import org.amshove.kluent.`should not be` +import org.amshove.kluent.`should throw` +import org.amshove.kluent.shouldEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class AvatarRequestHandlerTest { + private val mockSubsonicApiClient = mock() + private val handler = AvatarRequestHandler(mockSubsonicApiClient) + + @Test + fun `Should accept only cover art request`() { + val requestUri = createLoadAvatarRequest("some-username") + + handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo true + } + + @Test + fun `Should not accept random request uri`() { + val requestUri = Uri.Builder() + .scheme(SCHEME) + .authority(AUTHORITY) + .appendPath("something") + .build() + + handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo false + } + + @Test + fun `Should fail loading if uri doesn't contain username`() { + var requestUri = createLoadAvatarRequest("some-username") + requestUri = requestUri.buildUpon().clearQuery().build() + + val fail = { + handler.load(requestUri.buildRequest(), 0) + } + + fail `should throw` IllegalStateException::class + } + + @Test + fun `Should load avatar from network`() { + val streamResponse = StreamResponse( + loadResourceStream("Big_Buck_Bunny.jpeg"), + apiError = null, + responseHttpCode = 200 + ) + whenever(mockSubsonicApiClient.getAvatar(any())) + .thenReturn(streamResponse) + + val response = handler.load(createLoadAvatarRequest("some-username").buildRequest(), 0) + + response.loadedFrom `should equal` Picasso.LoadedFrom.NETWORK + response.source `should not be` null + } + + private fun Uri.buildRequest() = Request.Builder(this).build() +} diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt index 09927850..6572028d 100644 --- a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt @@ -11,8 +11,16 @@ class RequestCreatorTest { @Test fun `Should create valid load cover art request`() { val entityId = "299" - val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$COVER_ART_PATH?id=$entityId") + val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$COVER_ART_PATH?$QUERY_ID=$entityId") createLoadCoverArtRequest(entityId).compareTo(expectedUri).shouldEqualTo(0) } + + @Test + fun `Should create valid avatar request`() { + val username = "some-username" + val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$AVATAR_PATH?$QUERY_USERNAME=$username") + + createLoadAvatarRequest(username).compareTo(expectedUri).shouldEqualTo(0) + } } diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt new file mode 100644 index 00000000..afce254e --- /dev/null +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt @@ -0,0 +1,34 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import com.squareup.picasso.Picasso +import com.squareup.picasso.Request +import com.squareup.picasso.RequestHandler +import okio.Okio +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import java.io.IOException + +/** + * Loads avatars from subsonic api. + */ +class AvatarRequestHandler( + private val apiClient: SubsonicAPIClient +) : RequestHandler() { + override fun canHandleRequest(data: Request): Boolean { + return with(data.uri) { + scheme == SCHEME && + authority == AUTHORITY && + path == "/$AVATAR_PATH" + } + } + + override fun load(request: Request, networkPolicy: Int): Result { + val username = request.uri.getQueryParameter(QUERY_USERNAME) + + val response = apiClient.getAvatar(username) + if (response.hasError()) { + throw IOException("${response.apiError}") + } else { + return Result(Okio.source(response.stream), Picasso.LoadedFrom.NETWORK) + } + } +} diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt index 7d898437..8c6f4cd3 100644 --- a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt @@ -20,7 +20,7 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request } override fun load(request: Request, networkPolicy: Int): Result { - val id = request.uri.getQueryParameter("id") + val id = request.uri.getQueryParameter(QUERY_ID) val response = apiClient.getCoverArt(id) if (response.hasError()) { diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt index eb300fe5..9cecb7e3 100644 --- a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt @@ -5,10 +5,20 @@ import android.net.Uri internal const val SCHEME = "subsonic_api" internal const val AUTHORITY = BuildConfig.APPLICATION_ID internal const val COVER_ART_PATH = "cover_art" +internal const val AVATAR_PATH = "avatar" +internal const val QUERY_ID = "id" +internal const val QUERY_USERNAME = "username" internal fun createLoadCoverArtRequest(entityId: String): Uri = Uri.Builder() .scheme(SCHEME) .authority(AUTHORITY) .appendPath(COVER_ART_PATH) - .appendQueryParameter("id", entityId) + .appendQueryParameter(QUERY_ID, entityId) + .build() + +internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder() + .scheme(SCHEME) + .authority(AUTHORITY) + .appendPath(AVATAR_PATH) + .appendQueryParameter(QUERY_USERNAME, username) .build() diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt index 0f73ae2f..630bbc4a 100644 --- a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt @@ -3,6 +3,7 @@ package org.moire.ultrasonic.subsonic.loader.image import android.content.Context import android.widget.ImageView import com.squareup.picasso.Picasso +import com.squareup.picasso.RequestCreator import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient class SubsonicImageLoader( @@ -10,27 +11,44 @@ class SubsonicImageLoader( apiClient: SubsonicAPIClient ) { private val picasso = Picasso.Builder(context) - .addRequestHandler(CoverArtRequestHandler(apiClient)) - .build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) } + .addRequestHandler(CoverArtRequestHandler(apiClient)) + .addRequestHandler(AvatarRequestHandler(apiClient)) + .build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) } fun load(request: ImageRequest) = when (request) { is ImageRequest.CoverArt -> loadCoverArt(request) + is ImageRequest.Avatar -> loadAvatar(request) } private fun loadCoverArt(request: ImageRequest.CoverArt) { picasso.load(createLoadCoverArtRequest(request.entityId)) - .apply { - if (request.placeHolderDrawableRes != null) { - placeholder(request.placeHolderDrawableRes) - } - } - .apply { - if (request.errorDrawableRes != null) { - error(request.errorDrawableRes) - } - } + .addPlaceholder(request) + .addError(request) .into(request.imageView) } + + private fun loadAvatar(request: ImageRequest.Avatar) { + picasso.load(createLoadAvatarRequest(request.username)) + .addPlaceholder(request) + .addError(request) + .into(request.imageView) + } + + private fun RequestCreator.addPlaceholder(request: ImageRequest): RequestCreator { + if (request.placeHolderDrawableRes != null) { + placeholder(request.placeHolderDrawableRes) + } + + return this + } + + private fun RequestCreator.addError(request: ImageRequest): RequestCreator { + if (request.errorDrawableRes != null) { + error(request.errorDrawableRes) + } + + return this + } } sealed class ImageRequest( @@ -48,4 +66,15 @@ sealed class ImageRequest( errorDrawableRes, imageView ) + + class Avatar( + val username: String, + imageView: ImageView, + placeHolderDrawableRes: Int? = null, + errorDrawableRes: Int? = null + ) : ImageRequest( + placeHolderDrawableRes, + errorDrawableRes, + imageView + ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt index 83fb4d83..4ed49943 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt @@ -41,4 +41,25 @@ class SubsonicImageLoaderProxy( subsonicImageLoader.load(request) } } + + override fun loadAvatarImage( + view: View?, + username: String?, + large: Boolean, + size: Int, + crossFade: Boolean, + highQuality: Boolean + ) { + if (username != null && + view != null && + view is ImageView) { + val request = ImageRequest.Avatar( + username, + view, + placeHolderDrawableRes = R.drawable.ic_contact_picture, + errorDrawableRes = R.drawable.ic_contact_picture + ) + subsonicImageLoader.load(request) + } + } } From a63c47112ce8e0f3be8b9fada2513602e17267ef Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sat, 14 Jul 2018 22:14:20 +0200 Subject: [PATCH 9/9] Add feature flags that contains new image loader flag. This allows to release new versions of the app with unfinished/ not fully implemented features. Signed-off-by: Yahor Berdnikau --- .../activity/SubsonicTabActivity.java | 29 +++++++++++---- .../ultrasonic/fragment/SettingsFragment.java | 37 ++++++++++++------- .../org/moire/ultrasonic/util/Constants.java | 1 + .../kotlin/org/moire/ultrasonic/app/UApp.kt | 10 +++++ .../moire/ultrasonic/di/FeatureFlagsModule.kt | 11 ++++++ .../moire/ultrasonic/featureflags/Feature.kt | 14 +++++++ .../ultrasonic/featureflags/FeatureStorage.kt | 31 ++++++++++++++++ ultrasonic/src/main/res/values-es/strings.xml | 5 +++ ultrasonic/src/main/res/values-fr/strings.xml | 6 +++ ultrasonic/src/main/res/values-hu/strings.xml | 5 +++ .../src/main/res/values-pt-rBR/strings.xml | 5 +++ ultrasonic/src/main/res/values-pt/strings.xml | 5 +++ ultrasonic/src/main/res/values/strings.xml | 6 +++ ultrasonic/src/main/res/xml/settings.xml | 9 +++++ 14 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/FeatureFlagsModule.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/featureflags/Feature.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/featureflags/FeatureStorage.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java index cd7fcf46..be2783af 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -44,6 +44,7 @@ import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.domain.Share; +import org.moire.ultrasonic.featureflags.Feature; import org.moire.ultrasonic.service.*; import org.moire.ultrasonic.subsonic.SubsonicImageLoaderProxy; import org.moire.ultrasonic.util.*; @@ -790,10 +791,14 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen } } - public synchronized void clearImageLoader() - { - if (IMAGE_LOADER != null && IMAGE_LOADER.isRunning()) IMAGE_LOADER.clear(); - } + public synchronized void clearImageLoader() { + if (IMAGE_LOADER != null && + IMAGE_LOADER.isRunning()) { + IMAGE_LOADER.clear(); + } + + IMAGE_LOADER = null; + } public synchronized ImageLoader getImageLoader() { if (IMAGE_LOADER == null || @@ -802,10 +807,18 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen this, Util.getImageLoaderConcurrency(this) ); - IMAGE_LOADER = new SubsonicImageLoaderProxy( - legacyImageLoader, - ((UApp) getApplication()).getSubsonicImageLoader() - ); + + boolean isNewImageLoaderEnabled = ((UApp) getApplication()).getFeaturesStorage() + .isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER); + if (isNewImageLoaderEnabled) { + IMAGE_LOADER = new SubsonicImageLoaderProxy( + legacyImageLoader, + ((UApp) getApplication()).getSubsonicImageLoader() + ); + } else { + IMAGE_LOADER = legacyImageLoader; + } + IMAGE_LOADER.startImageLoader(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index 472c49d1..98feabc8 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -4,29 +4,21 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.EditTextPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceCategory; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; +import android.preference.*; import android.provider.SearchRecentSuggestions; import android.support.annotation.Nullable; import android.util.Log; import android.view.View; - import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.ServerSettingsActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity; +import org.moire.ultrasonic.app.UApp; +import org.moire.ultrasonic.featureflags.Feature; +import org.moire.ultrasonic.featureflags.FeatureStorage; import org.moire.ultrasonic.provider.SearchSuggestionProvider; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.FileUtil; -import org.moire.ultrasonic.util.ImageLoader; -import org.moire.ultrasonic.util.TimeSpanPreference; -import org.moire.ultrasonic.util.Util; +import org.moire.ultrasonic.util.*; import java.io.File; @@ -115,6 +107,7 @@ public class SettingsFragment extends PreferenceFragment sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity())); setupClearSearchPreference(); setupGaplessControlSettingsV14(); + setupFeatureFlagsPreferences(); } @Override @@ -178,6 +171,24 @@ public class SettingsFragment extends PreferenceFragment } } + private void setupFeatureFlagsPreferences() { + CheckBoxPreference ffImageLoader = (CheckBoxPreference) findPreference( + Constants.PREFERENCES_KEY_FF_IMAGE_LOADER); + + final FeatureStorage featureStorage = ((UApp) getActivity().getApplication()).getFeaturesStorage(); + if (ffImageLoader != null) { + ffImageLoader.setChecked(featureStorage.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER)); + ffImageLoader.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object o) { + featureStorage.changeFeatureFlag(Feature.NEW_IMAGE_DOWNLOADER, (Boolean) o); + ((SubsonicTabActivity) getActivity()).clearImageLoader(); + return true; + } + }); + } + } + private void setupGaplessControlSettingsV14() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { PreferenceCategory playbackControlSettings = diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java index e183f878..2fdb0c79 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java @@ -130,6 +130,7 @@ public final class Constants public static final String PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST = "showAllSongsByArtist"; public static final String PREFERENCES_KEY_SCAN_MEDIA = "scanMedia"; public static final String PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY = "imageLoaderConcurrency"; + public static final String PREFERENCES_KEY_FF_IMAGE_LOADER = "ff_new_image_loader"; // Number of free trial days for non-licensed servers. public static final int FREE_TRIAL_DAYS = 30; diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index 97fcc8bb..8eb735b6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -5,7 +5,9 @@ import org.koin.android.ext.android.get import org.koin.android.ext.android.startKoin import org.moire.ultrasonic.di.baseNetworkModule import org.moire.ultrasonic.di.directoriesModule +import org.moire.ultrasonic.di.featureFlagsModule import org.moire.ultrasonic.di.musicServiceModule +import org.moire.ultrasonic.featureflags.FeatureStorage import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader import org.moire.ultrasonic.util.Util @@ -17,6 +19,7 @@ class UApp : Application() { startKoin(this, listOf( directoriesModule, baseNetworkModule, + featureFlagsModule(this), musicServiceModule(sharedPreferences, this) )) } @@ -27,4 +30,11 @@ class UApp : Application() { fun getSubsonicImageLoader(): SubsonicImageLoader { return get() } + + /** + * Temporary method to get features storage. + */ + fun getFeaturesStorage(): FeatureStorage { + return get() + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/FeatureFlagsModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/FeatureFlagsModule.kt new file mode 100644 index 00000000..81a1b09f --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/FeatureFlagsModule.kt @@ -0,0 +1,11 @@ +package org.moire.ultrasonic.di + +import android.content.Context +import org.koin.dsl.module.applicationContext +import org.moire.ultrasonic.featureflags.FeatureStorage + +fun featureFlagsModule( + context: Context +) = applicationContext { + factory { FeatureStorage(context) } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/featureflags/Feature.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/featureflags/Feature.kt new file mode 100644 index 00000000..1f0757a3 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/featureflags/Feature.kt @@ -0,0 +1,14 @@ +package org.moire.ultrasonic.featureflags + +/** + * Contains a list of new features/implementations that are not yet finished, + * but possible to try it out. + */ +enum class Feature( + val defaultValue: Boolean +) { + /** + * Enables new image downloader implementation. + */ + NEW_IMAGE_DOWNLOADER(false) +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/featureflags/FeatureStorage.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/featureflags/FeatureStorage.kt new file mode 100644 index 00000000..dca9bb4f --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/featureflags/FeatureStorage.kt @@ -0,0 +1,31 @@ +package org.moire.ultrasonic.featureflags + +import android.content.Context + +private const val SP_NAME = "feature_flags" + +/** + * Provides storage for current feature flag state. + */ +class FeatureStorage( + context: Context +) { + private val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) + + /** + * Get [feature] current enabled state. + */ + fun isFeatureEnabled(feature: Feature): Boolean { + return sp.getBoolean(feature.name, feature.defaultValue) + } + + /** + * Update [feature] enabled state to [isEnabled]. + */ + fun changeFeatureFlag( + feature: Feature, + isEnabled: Boolean + ) { + sp.edit().putBoolean(feature.name, isEnabled).apply() + } +} diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 9ef97bff..58599912 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -436,5 +436,10 @@ El período de prueba ha terminado. Versiones incompatibles. Por favor actualiza la aplicación de Android UltraSonic. Versiones incompatibles. Por favor actualiza el servidor de Subsonic. + Banderas de características + Permite la implementación de un nuevo cargador de imágenes. + Actualmente no guarda la imagen en el almacenamiento del dispositivo y sólo utiliza caché en la memoria. + + Habilitar nuevo cargador de imágenes diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index f2ca90fc..ddd81d03 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -436,5 +436,11 @@ La période d\'essai est terminée. Versions incompatible. Veuillez mette à jour l\'application Android UltraSonic. Versions incompatible. Veuillez mette à jour le serveur Subsonic. + Drapeaux des fonctionnalités + Permet l\'implémentation d\'un nouveau chargeur d\'images. + Actuellement, il n\'enregistre pas l\'image dans le stockage de l\'appareil et n\'utilise que le cache en + mémoire. + + Activer le nouveau chargeur d\'images diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 9bfe728e..bca57a9c 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -436,5 +436,10 @@ A próbaidő vége. Nem kompatibilis verzió. Kérjük, frissítse az UltraSonic Android alkalmazást! Nem kompatibilis verzió. Kérjük, frissítse a Subsonic kiszolgálót! + Engedélyezi az új képbetöltő megvalósítását. Jelenleg nem + tárolja a képet az eszköz tárolójában, és csak a memóriában tárolja a gyorsítótárat. + + Jellemzők Zászlók + Engedélyezzen új képbetöltőt diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index a6085eda..4c0c6dfa 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -436,5 +436,10 @@ O período de avaliação acabou. Versões incompativeis. Atualize o aplicativo UltraSonic para Android. Versões incompativeis. Atualize o servidor UltraSonic. + Permite nova implementação do carregador de imagens. + Atualmente, ele não salva a imagem no armazenamento do dispositivo e usa apenas o cache na memória. + + Bandeiras de recursos + Ativar novo carregador de imagens diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index d5f1fcaf..784e116f 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -436,5 +436,10 @@ O período de avaliação acabou. Versões incompativeis. Atualize o aplicativo UltraSonic para Android. Versões incompativeis. Atualize o servidor UltraSonic. + Permite nova implementação do carregador de imagens. + Atualmente, ele não salva a imagem no armazenamento do dispositivo e usa apenas o cache na memória. + + Bandeiras de recursos + Ativar novo carregador de imagens diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index b9edf0d0..fe80ab0c 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -441,4 +441,10 @@ Incompatible versions. Please upgrade UltraSonic Android app. Incompatible versions. Please upgrade Subsonic server. + Enable new image loader + Enables new image loader implementation. + Currently it doesn\'t save image in device storage and uses only cache in memory. + + Feature Flags + diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index e66f2b7e..a4a52386 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -284,5 +284,14 @@ a:summary="@string/settings.screen_lit_summary" a:title="@string/settings.screen_lit_title"/> + + + + \ No newline at end of file