Skip to content

Commit c3b3348

Browse files
committed
Initial commit
0 parents  commit c3b3348

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1401
-0
lines changed

.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea/caches
5+
/.idea/libraries
6+
/.idea/modules.xml
7+
/.idea/workspace.xml
8+
/.idea/navEditor.xml
9+
/.idea/assetWizardSettings.xml
10+
.DS_Store
11+
/build
12+
/captures
13+
.externalNativeBuild
14+
.cxx
15+
local.properties
16+
17+
# IDE folders #
18+
gen/
19+
bin/
20+
out/
21+
.idea/*
22+
!.idea/codeStyleSettings.xml
23+
!.idea/encodings.xml
24+
!.idea/inspectionProfiles/
25+
26+
# Gradle files
27+
.gradle/
28+
*build/

app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

app/build.gradle

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
plugins {
2+
id 'com.android.application'
3+
id 'kotlin-android'
4+
}
5+
6+
android {
7+
compileSdk 30
8+
9+
defaultConfig {
10+
applicationId "com.tomclaw.imageloader"
11+
minSdk 21
12+
targetSdk 30
13+
versionCode 1
14+
versionName "1.0"
15+
16+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17+
}
18+
19+
buildTypes {
20+
release {
21+
minifyEnabled false
22+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23+
}
24+
}
25+
compileOptions {
26+
sourceCompatibility JavaVersion.VERSION_1_8
27+
targetCompatibility JavaVersion.VERSION_1_8
28+
}
29+
kotlinOptions {
30+
jvmTarget = '1.8'
31+
}
32+
}
33+
34+
dependencies {
35+
implementation 'androidx.core:core-ktx:1.6.0'
36+
implementation 'androidx.appcompat:appcompat:1.3.1'
37+
implementation 'com.google.android.material:material:1.4.0'
38+
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
39+
implementation 'com.github.solkin:disk-lru-cache:1.4'
40+
testImplementation 'junit:junit:4.+'
41+
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
42+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
43+
}

app/proguard-rules.pro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.tomclaw.imageloader
2+
3+
import androidx.test.platform.app.InstrumentationRegistry
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
6+
import org.junit.Test
7+
import org.junit.runner.RunWith
8+
9+
import org.junit.Assert.*
10+
11+
/**
12+
* Instrumented test, which will execute on an Android device.
13+
*
14+
* See [testing documentation](http://d.android.com/tools/testing).
15+
*/
16+
@RunWith(AndroidJUnit4::class)
17+
class ExampleInstrumentedTest {
18+
@Test
19+
fun useAppContext() {
20+
// Context of the app under test.
21+
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22+
assertEquals("com.tomclaw.imageloader", appContext.packageName)
23+
}
24+
}

app/src/main/AndroidManifest.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.tomclaw.imageloader">
4+
5+
<uses-permission android:name="android.permission.INTERNET" />
6+
7+
<application
8+
android:allowBackup="true"
9+
android:icon="@mipmap/ic_launcher"
10+
android:label="@string/app_name"
11+
android:roundIcon="@mipmap/ic_launcher_round"
12+
android:supportsRtl="true"
13+
android:theme="@style/Theme.SimpleImageLoader">
14+
<activity
15+
android:name=".MainActivity"
16+
android:exported="true">
17+
<intent-filter>
18+
<action android:name="android.intent.action.MAIN" />
19+
20+
<category android:name="android.intent.category.LAUNCHER" />
21+
</intent-filter>
22+
</activity>
23+
</application>
24+
25+
</manifest>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.tomclaw.imageloader
2+
3+
import android.graphics.Bitmap
4+
import android.graphics.BitmapFactory
5+
import android.graphics.Matrix
6+
import android.media.ExifInterface
7+
import java.io.BufferedInputStream
8+
import java.io.File
9+
import java.io.FileInputStream
10+
import java.io.IOException
11+
import java.io.InputStream
12+
13+
interface BitmapDecoder {
14+
15+
fun getBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap?
16+
17+
}
18+
19+
class BitmapDecoderImpl() : BitmapDecoder {
20+
21+
override fun getBitmap(
22+
file: File,
23+
reqWidth: Int,
24+
reqHeight: Int
25+
): Bitmap? {
26+
var bitmap: Bitmap?
27+
var inputStream: InputStream? = null
28+
try {
29+
inputStream = FileInputStream(file)
30+
bitmap = decodeSampledBitmapFromStream(inputStream, reqWidth, reqHeight)
31+
val rotation = getRotation(file)
32+
if (bitmap != null && rotation != 0) {
33+
val width = bitmap.width
34+
val height = bitmap.height
35+
val m = Matrix()
36+
m.setRotate(rotation.toFloat(), (width / 2).toFloat(), (height / 2).toFloat())
37+
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, m, false)
38+
}
39+
} catch (ignored: Throwable) {
40+
ignored.printStackTrace()
41+
bitmap = null
42+
} finally {
43+
if (inputStream != null) {
44+
try {
45+
inputStream.close()
46+
} catch (ignored: IOException) {
47+
}
48+
}
49+
}
50+
return bitmap
51+
}
52+
53+
private fun decodeSampledBitmapFromStream(
54+
stream: InputStream,
55+
reqWidth: Int,
56+
reqHeight: Int
57+
): Bitmap? {
58+
var bitmap: Bitmap?
59+
try {
60+
val inputStream: InputStream = BufferedInputStream(stream, THUMBNAIL_BUFFER_SIZE)
61+
inputStream.mark(THUMBNAIL_BUFFER_SIZE)
62+
63+
// First decode with inJustDecodeBounds=true to check dimensions
64+
val options = BitmapFactory.Options()
65+
options.inJustDecodeBounds = true
66+
BitmapFactory.decodeStream(inputStream, null, options)
67+
68+
// Calculate inSampleSize
69+
val widthSample = (options.outWidth / reqWidth).toFloat()
70+
val heightSample = (options.outHeight / reqHeight).toFloat()
71+
var scaleFactor = Math.max(widthSample, heightSample)
72+
if (scaleFactor < 1) {
73+
scaleFactor = 1f
74+
}
75+
options.inJustDecodeBounds = false
76+
options.inSampleSize = scaleFactor.toInt()
77+
options.inPreferredConfig = Bitmap.Config.RGB_565
78+
79+
// Decode bitmap with inSampleSize set
80+
inputStream.reset()
81+
bitmap = BitmapFactory.decodeStream(inputStream, null, options)
82+
} catch (ignored: Throwable) {
83+
ignored.printStackTrace()
84+
bitmap = null
85+
}
86+
return bitmap
87+
}
88+
89+
private fun getRotation(file: File): Int {
90+
return when (obtainFileOrientation(file.absolutePath)) {
91+
ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSPOSE -> 90
92+
ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_VERTICAL -> 180
93+
ExifInterface.ORIENTATION_ROTATE_270, ExifInterface.ORIENTATION_TRANSVERSE -> 270
94+
else -> 0
95+
}
96+
}
97+
98+
private fun obtainFileOrientation(fileName: String): Int {
99+
return try {
100+
val exifInterface = ExifInterface(fileName)
101+
exifInterface.getAttributeInt(
102+
ExifInterface.TAG_ORIENTATION,
103+
ExifInterface.ORIENTATION_NORMAL
104+
)
105+
} catch (ex: IOException) {
106+
ExifInterface.ORIENTATION_UNDEFINED
107+
}
108+
}
109+
110+
}
111+
112+
/**
113+
* Buffer is large enough to rewind past any EXIF headers.
114+
*/
115+
private const val THUMBNAIL_BUFFER_SIZE = 128 * 1024
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.tomclaw.imageloader
2+
3+
import com.tomclaw.cache.DiskLruCache
4+
import java.io.File
5+
6+
interface DiskCache {
7+
8+
fun get(key: String): File?
9+
10+
fun put(key: String, file: File): File
11+
12+
fun remove(key: String)
13+
14+
}
15+
16+
class DiskCacheImpl(private val diskLruCache: DiskLruCache): DiskCache {
17+
18+
override fun get(key: String): File? {
19+
return diskLruCache[key]
20+
}
21+
22+
override fun put(key: String, file: File): File {
23+
return diskLruCache.put(key, file)
24+
}
25+
26+
override fun remove(key: String) {
27+
diskLruCache.delete(key)
28+
}
29+
30+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.tomclaw.imageloader
2+
3+
import java.io.File
4+
import java.io.FileOutputStream
5+
import java.net.HttpURLConnection
6+
import java.net.URL
7+
8+
interface FileDownloader {
9+
10+
fun download(url: String, file: File): Boolean
11+
12+
}
13+
14+
class FileDownloaderImpl() : FileDownloader {
15+
16+
override fun download(url: String, file: File): Boolean {
17+
val connection = openConnection(url)
18+
val fileStream = FileOutputStream(file)
19+
return connection.inputStream
20+
?.takeIf { connection.responseCode in 200..299 }
21+
?.safeCopyTo(fileStream)
22+
?.takeIf { true } ?: false
23+
}
24+
25+
private fun openConnection(url: String): HttpURLConnection {
26+
val u = URL(url)
27+
return (u.openConnection() as HttpURLConnection).apply {
28+
requestMethod = METHOD_GET
29+
doInput = true
30+
doOutput = false
31+
connectTimeout = TIMEOUT_CONNECTION
32+
readTimeout = TIMEOUT_SOCKET
33+
}
34+
}
35+
36+
}
37+
38+
private const val METHOD_GET = "GET"
39+
private const val TIMEOUT_SOCKET = 70 * 1000
40+
private const val TIMEOUT_CONNECTION = 60 * 1000
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.tomclaw.imageloader
2+
3+
import android.net.Uri
4+
import java.io.File
5+
import java.io.IOException
6+
7+
interface FileProvider {
8+
9+
fun getFile(uri: Uri): File? {
10+
return when (uri.scheme) {
11+
"http", "https" -> getFile(uri.path.orEmpty())
12+
else -> null
13+
}
14+
}
15+
16+
fun getFile(url: String): File?
17+
18+
}
19+
20+
class FileProviderImpl(
21+
private val cacheDir: File,
22+
private val diskCache: DiskCache,
23+
private val fileDownloader: FileDownloader
24+
) : FileProvider {
25+
26+
override fun getFile(url: String): File? {
27+
return diskCache.get(url) ?: downloadIntoCache(url)
28+
}
29+
30+
private fun downloadIntoCache(url: String): File? {
31+
var tempFile: File? = null
32+
try {
33+
tempFile = File.createTempFile("file", ".tmp", cacheDir)
34+
return fileDownloader.download(url, tempFile)
35+
.takeIf { true }
36+
?.let { diskCache.put(url, tempFile) }
37+
} catch (ex: IOException) {
38+
ex.printStackTrace()
39+
} finally {
40+
tempFile?.delete()
41+
}
42+
return null
43+
}
44+
45+
}

0 commit comments

Comments
 (0)