diff --git a/app/build.gradle b/app/build.gradle index 6f11855..860aaf8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -125,6 +125,9 @@ dependencies { exclude group: 'com.android.support.constraint', module: 'constraint-layout' } + // backport Future support + implementation 'net.sourceforge.streamsupport:android-retrofuture:1.7.3' + implementation 'org.greenrobot:eventbus:3.2.0' implementation 'me.weishu:free_reflection:3.0.1' diff --git a/app/src/main/java/com/nemesiss/dev/piaprobox/Activity/Music/MusicPlayerActivity.kt b/app/src/main/java/com/nemesiss/dev/piaprobox/Activity/Music/MusicPlayerActivity.kt index 1d83404..db0c727 100644 --- a/app/src/main/java/com/nemesiss/dev/piaprobox/Activity/Music/MusicPlayerActivity.kt +++ b/app/src/main/java/com/nemesiss/dev/piaprobox/Activity/Music/MusicPlayerActivity.kt @@ -4,6 +4,7 @@ import android.app.AlertDialog import android.content.Intent import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable +import android.net.Uri import android.os.Bundle import android.support.v4.view.ViewCompat import android.support.v7.widget.LinearLayoutManager @@ -29,11 +30,14 @@ import com.nemesiss.dev.piaprobox.model.MusicPlayerActivityStatus import com.nemesiss.dev.piaprobox.R import com.nemesiss.dev.piaprobox.Service.AsyncExecutor import com.nemesiss.dev.piaprobox.Service.DaggerFactory.DaggerDownloadServiceFactory +import com.nemesiss.dev.piaprobox.Service.DaggerFactory.DaggerImageCacheFactory import com.nemesiss.dev.piaprobox.Service.DaggerModules.DownloadServiceModules +import com.nemesiss.dev.piaprobox.Service.DaggerModules.ImageCacheModules import com.nemesiss.dev.piaprobox.Service.Download.DownloadService import com.nemesiss.dev.piaprobox.Service.GlideApp import com.nemesiss.dev.piaprobox.Service.HTMLParser import com.nemesiss.dev.piaprobox.Service.HTMLParser.Companion.GetAlbumThumb +import com.nemesiss.dev.piaprobox.Service.ImageCache import com.nemesiss.dev.piaprobox.Service.Player.MusicPlayerService import com.nemesiss.dev.piaprobox.Service.Player.NewPlayer.PlayerAction import com.nemesiss.dev.piaprobox.Service.SimpleHTTP.DaggerFetchFactory @@ -226,6 +230,9 @@ open class MusicPlayerActivity : PiaproboxBaseActivity() { @Inject lateinit var downloadService: DownloadService + @Inject + lateinit var imageCache: ImageCache + private lateinit var htmlParser: HTMLParser protected var relatedMusicListData: List? = null private var relatedMusicListAdapter: RelatedMusicListAdapter? = null @@ -262,17 +269,16 @@ open class MusicPlayerActivity : PiaproboxBaseActivity() { isFirstResource: Boolean ): Boolean { if (resource != null) { - keepLastMusicBitmap(resource) + val thumbUrl = CurrentMusicPlayInfo?.Thumb ?: return false + val fileName = Uri.parse(thumbUrl).lastPathSegment ?: return false + imageCache.cache(resource, fileName) + log.info("Image $fileName cached!") } return false } } - private fun keepLastMusicBitmap(drawable: Drawable) { - LAST_MUSIC_BITMAP = drawable.constantState?.newDrawable()?.mutate() - } - override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) HandleSwitchMusicIntent(intent) @@ -288,6 +294,12 @@ open class MusicPlayerActivity : PiaproboxBaseActivity() { .build() .inject(this) + DaggerImageCacheFactory + .builder() + .imageCacheModules(ImageCacheModules(this)) + .build() + .inject(this) + ShowToolbarBackIcon(MusicPlayer_Toolbar) // 关闭RecyclerView的嵌套滚动 ViewCompat.setNestedScrollingEnabled(MusicPlayer_Lyric_RecyclerView, false) @@ -336,18 +348,8 @@ open class MusicPlayerActivity : PiaproboxBaseActivity() { CurrentPlayItemIndex = activityStatus.currentPlayItemIndex PLAY_LISTS = activityStatus.playLists - val lastThumbBitmap = LAST_MUSIC_BITMAP - if (lastThumbBitmap != null) { - log.info("Find valid lastThumbBitmap, reload.") - GlideApp.with(this) - .load(lastThumbBitmap) - .priority(Priority.HIGH) - .into(MusicPlayer_ThumbBackground) - keepLastMusicBitmap(lastThumbBitmap) - } else { - log.info("No valid lastThumbBitmap, load thumb from network. {}.", CurrentMusicPlayInfo?.Thumb) - GlideLoadThumbToImageView(CurrentMusicPlayInfo?.Thumb ?: "") - } + GlideLoadThumbToImageView(CurrentMusicPlayInfo?.Thumb ?: "") + ActivateLyricRecyclerViewAdapter() ActivateRelatedMusicRecyclerViewAdapter() } @@ -425,14 +427,29 @@ open class MusicPlayerActivity : PiaproboxBaseActivity() { } private fun GlideLoadThumbToImageView(url: String) { + + // first, touch cache for previous result. + val fileName = Uri.parse(url).lastPathSegment + if (fileName != null) { + val cacheImage = imageCache[fileName] + log.warn("Image $fileName hit cache!") + if (cacheImage != null) { + GlideApp.with(this) + .load(cacheImage) + .priority(Priority.HIGH) + .into(MusicPlayer_ThumbBackground) + log.warn("Image $fileName loaded from cache!") + return + } + } + log.warn("Image $fileName has no cache, load ahead.") try { GlideApp.with(this) .load(url) .priority(Priority.HIGH) .addListener(MUSIC_ALBUM_LOAD_LISTENER) .into(MusicPlayer_ThumbBackground) - } catch (e: Exception) { - } + } catch (e: Exception) { } } private fun playMusic(contentInfo: MusicContentInfo, playInfo: MusicPlayInfo) { @@ -578,9 +595,6 @@ open class MusicPlayerActivity : PiaproboxBaseActivity() { @JvmStatic var LAST_MUSIC_PLAYER_ACTIVITY_STATUS: MusicPlayerActivityStatus? = null - @JvmStatic - private var LAST_MUSIC_BITMAP: Drawable? = null - @JvmStatic var LAST_LOAD_CONTENT_URL = "" @@ -597,11 +611,6 @@ open class MusicPlayerActivity : PiaproboxBaseActivity() { // 音乐播放器没在播放,删掉当前播放列表 if (!MusicPlayerService.IS_FOREGROUND) { - val bitmap = (LAST_MUSIC_BITMAP as? BitmapDrawable)?.bitmap - if (bitmap?.isRecycled == false) { - bitmap.recycle() - } - LAST_MUSIC_BITMAP = null PLAY_LISTS = null playListCache.clear() diff --git a/app/src/main/java/com/nemesiss/dev/piaprobox/Service/DaggerFactory/ImageCacheFactory.java b/app/src/main/java/com/nemesiss/dev/piaprobox/Service/DaggerFactory/ImageCacheFactory.java new file mode 100644 index 0000000..5537c69 --- /dev/null +++ b/app/src/main/java/com/nemesiss/dev/piaprobox/Service/DaggerFactory/ImageCacheFactory.java @@ -0,0 +1,16 @@ +package com.nemesiss.dev.piaprobox.Service.DaggerFactory; + +import com.nemesiss.dev.piaprobox.Activity.Music.MusicPlayerActivity; +import com.nemesiss.dev.piaprobox.Service.DaggerModules.ImageCacheModules; +import dagger.Component; +import dagger.Provides; + +import javax.inject.Singleton; + +@Singleton +@Component(modules = {ImageCacheModules.class}) +public interface ImageCacheFactory { + + void inject(MusicPlayerActivity activity); + +} diff --git a/app/src/main/java/com/nemesiss/dev/piaprobox/Service/DaggerModules/ImageCacheModules.java b/app/src/main/java/com/nemesiss/dev/piaprobox/Service/DaggerModules/ImageCacheModules.java new file mode 100644 index 0000000..7412261 --- /dev/null +++ b/app/src/main/java/com/nemesiss/dev/piaprobox/Service/DaggerModules/ImageCacheModules.java @@ -0,0 +1,20 @@ +package com.nemesiss.dev.piaprobox.Service.DaggerModules; + +import android.content.Context; +import dagger.Module; +import dagger.Provides; + +@Module +public class ImageCacheModules { + + private Context context; + + public ImageCacheModules(Context context) { + this.context = context; + } + + @Provides + public Context getContext() { + return context; + } +} diff --git a/app/src/main/java/com/nemesiss/dev/piaprobox/Service/ImageCache.kt b/app/src/main/java/com/nemesiss/dev/piaprobox/Service/ImageCache.kt new file mode 100644 index 0000000..02958cb --- /dev/null +++ b/app/src/main/java/com/nemesiss/dev/piaprobox/Service/ImageCache.kt @@ -0,0 +1,112 @@ +package com.nemesiss.dev.piaprobox.Service + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Environment +import java9.util.concurrent.CompletableFuture +import okhttp3.internal.closeQuietly +import org.slf4j.getLogger +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.* +import java.util.concurrent.Future +import javax.inject.Inject + +class ImageCache @Inject constructor(private val context: Context) { + + private companion object { + private val log = getLogger() + } + + private val cacheDir = File(context.filesDir, "ImageCache") + private val async = AsyncExecutor.INSTANCE + + init { + if (!cacheDir.exists()) cacheDir.mkdirs() + } + + + fun cache(drawable: Drawable, fileName: String): Future { + if (drawable is BitmapDrawable) { + val bitmap = drawable.bitmap + return cache(bitmap, fileName) + } + val bounds = drawable.bounds + val width = bounds.right - bounds.left + val height = bounds.bottom - bounds.top + + if (width <= 0 || height <= 0) { + return CompletableFuture().apply { complete(false) } + } + + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + drawable.draw(canvas) + return cache(bitmap, fileName) + } + + fun cache(bitmap: Bitmap, fileName: String): Future { + val format = detectCompressFormat(fileName) + return async.SendTaskWithResult task@ { + // duplicate bitmap for safe. + val cloneBitmap = Bitmap.createBitmap(bitmap) + val fos = FileOutputStream(File(cacheDir, fileName)) + try { + cloneBitmap.compress(format, 100, fos) + return@task true + } catch (t: Throwable) { + log.error("Cannot save $fileName", t) + return@task false + } finally { + fos.closeQuietly() + cloneBitmap.recycle() + } + } + } + + fun cache(byteArray: ByteArray, fileName: String): Future { + // no used, just ensure extension exists in file name. + detectCompressFormat(fileName) + return async.SendTaskWithResult task@ { + val fos = FileOutputStream(File(cacheDir, fileName)) + try { + fos.write(byteArray) + return@task true + } catch (t: Throwable) { + log.error("Cannot save byte array for $fileName", t) + return@task false + } finally { + fos.closeQuietly() + } + } + } + + operator fun get(fileName: String): ByteArray? { + val cacheFile = File(cacheDir, fileName) + if (!cacheFile.exists()) return null + val fis = FileInputStream(cacheFile) + val result = fis.readBytes() + fis.closeQuietly() + return result + } + + private fun detectCompressFormat(fileName: String): Bitmap.CompressFormat { + val extDotIndex = fileName.lastIndexOf('.') + if (extDotIndex == -1) { + throw IllegalArgumentException("Filename $fileName must include format extension like png, jpg or webp.") + } + var extension = fileName.substring(extDotIndex + 1) + extension = extension.trim().toLowerCase(Locale.CHINA) + + return when (extension) { + "png" -> Bitmap.CompressFormat.PNG + "jpg", "jpeg" -> Bitmap.CompressFormat.JPEG + "webp" -> Bitmap.CompressFormat.WEBP + else -> throw IllegalArgumentException("Filename $fileName must include format extension like png, jpg or webp.") + } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/nemesiss/dev/piaprobox/CookieParserTest.kt b/app/src/test/java/com/nemesiss/dev/piaprobox/CookieParserTest.kt index e6c36d6..22bf5b0 100644 --- a/app/src/test/java/com/nemesiss/dev/piaprobox/CookieParserTest.kt +++ b/app/src/test/java/com/nemesiss/dev/piaprobox/CookieParserTest.kt @@ -35,5 +35,4 @@ class CookieParserTest { assertFalse(cookie.isSecure) asEquals("/", cookie.path) } - } diff --git a/app/src/test/java/com/nemesiss/dev/piaprobox/UriTest.kt b/app/src/test/java/com/nemesiss/dev/piaprobox/UriTest.kt new file mode 100644 index 0000000..b86b16f --- /dev/null +++ b/app/src/test/java/com/nemesiss/dev/piaprobox/UriTest.kt @@ -0,0 +1,10 @@ +package com.nemesiss.dev.piaprobox + +import android.net.Uri +import org.junit.Assert +import org.junit.Test +import java.net.URL + +class UriTest { + +} \ No newline at end of file