diff --git a/app/build.gradle b/app/build.gradle index 3a59b113a..966ba8516 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,12 @@ kapt { } } +kotlin{ + experimental{ + coroutines "enable" + } +} + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" @@ -68,12 +74,17 @@ dependencies { implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version" //协程 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' - + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' + //规则相关 implementation 'pub.devrel:easypermissions:3.0.0' implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.jayway.jsonpath:json-path:2.4.0' implementation 'org.jsoup:jsoup:1.12.1' + //Retrofit + implementation 'com.squareup.okhttp3:logging-interceptor:3.14.0'// + implementation 'com.squareup.retrofit2:retrofit:2.5.0' + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/app/src/main/java/io/legado/app/base/BaseViewModel.kt b/app/src/main/java/io/legado/app/base/BaseViewModel.kt new file mode 100644 index 000000000..009aafe7d --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BaseViewModel.kt @@ -0,0 +1,58 @@ +package io.legado.app.base + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import kotlinx.coroutines.* +import kotlin.coroutines.CoroutineContext + +open class BaseViewModel(application: Application) : AndroidViewModel(application), CoroutineScope { + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + + + private val launchManager: MutableList = mutableListOf() + + protected fun launchOnUI( + tryBlock: suspend CoroutineScope.() -> Unit, + cacheBlock: suspend CoroutineScope.(Throwable) -> Unit, + finallyBlock: suspend CoroutineScope.() -> Unit, + handleCancellationExceptionManually: Boolean + ) { + launchOnUI { + tryCatch(tryBlock, cacheBlock, finallyBlock, handleCancellationExceptionManually) + } + } + + /** + * add launch task to [launchManager] + */ + private fun launchOnUI(block: suspend CoroutineScope.() -> Unit) { + val job = launch { block() } + launchManager.add(job) + job.invokeOnCompletion { launchManager.remove(job) } + } + + private suspend fun tryCatch( + tryBlock: suspend CoroutineScope.() -> Unit, + catchBlock: suspend CoroutineScope.(Throwable) -> Unit, + finallyBlock: suspend CoroutineScope.() -> Unit, + handleCancellationExceptionManually: Boolean = false + ) { + try { + coroutineScope { tryBlock() } + } catch (e: Throwable) { + if (e !is CancellationException || handleCancellationExceptionManually) { + coroutineScope { catchBlock(e) } + } else { + throw e + } + } finally { + coroutineScope { finallyBlock() } + } + } + + override fun onCleared() { + super.onCleared() + launchManager.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt index c9b9f0cf2..ce7b3d4a6 100644 --- a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt +++ b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt @@ -257,7 +257,7 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec return footerItems?.size() ?: 0 } - fun getItem(position: Int): ITEM = items[position % items.size] + fun getItem(position: Int): ITEM? = if (position in 0 until items.size) items[position] else null fun getItems(): List = items @@ -280,7 +280,7 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec return when { isHeader(position) -> TYPE_HEADER_VIEW + position isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount() - else -> getItemViewType(getItem(getRealPosition(position)), getRealPosition(position)) + else -> getItem(getActualPosition(position))?.let { getItemViewType(it, getActualPosition(position)) } ?: 0 } } @@ -299,13 +299,17 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec if (itemClickListener != null) { holder.itemView.setOnClickListener { - itemClickListener!!.invoke(holder, getItem(holder.layoutPosition)) + getItem(holder.layoutPosition)?.let { + itemClickListener?.invoke(holder, it) + } } } if (itemLongClickListener != null) { holder.itemView.setOnLongClickListener { - itemLongClickListener!!.invoke(holder, getItem(holder.layoutPosition)) + getItem(holder.layoutPosition)?.let { + itemLongClickListener?.invoke(holder, it) ?: true + } ?: true } } @@ -315,14 +319,15 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec } - override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { - onBindViewHolder(holder, position, mutableListOf()) + final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { } - override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList) { + final override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList) { if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { - itemDelegates.getValue(getItemViewType(holder.layoutPosition)) - .convert(holder, getItem(holder.layoutPosition), payloads) + getItem(holder.layoutPosition)?.let { + itemDelegates.getValue(getItemViewType(holder.layoutPosition)) + .convert(holder, it, payloads) + } } } @@ -339,15 +344,17 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec if (manager is GridLayoutManager) { manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { - return if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize( - getItem(position), getItemViewType(position), position - ) + return getItem(position)?.let { + if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize( + it, getItemViewType(position), position + ) + } ?: manager.spanCount } } } } - fun setAnimationConfig(item: ItemAnimation) { + fun setItemAnimation(item: ItemAnimation) { itemAnimation = item } @@ -359,7 +366,7 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec return position >= getActualItemCount() + getHeaderCount() } - private fun getRealPosition(position: Int): Int { + private fun getActualPosition(position: Int): Int { return position - getHeaderCount() } diff --git a/app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt b/app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt new file mode 100644 index 000000000..78e0aa7a0 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt @@ -0,0 +1,107 @@ +package io.legado.app.help.http + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import retrofit2.* +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class CoroutineCallAdapterFactory private constructor() : CallAdapter.Factory() { + companion object { + @JvmStatic @JvmName("create") + operator fun invoke() = CoroutineCallAdapterFactory() + } + + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit + ): CallAdapter<*, *>? { + if (Deferred::class.java != getRawType(returnType)) { + return null + } + if (returnType !is ParameterizedType) { + throw IllegalStateException( + "Deferred return type must be parameterized as Deferred or Deferred") + } + val responseType = getParameterUpperBound(0, returnType) + + val rawDeferredType = getRawType(responseType) + return if (rawDeferredType == Response::class.java) { + if (responseType !is ParameterizedType) { + throw IllegalStateException( + "Response must be parameterized as Response or Response") + } + ResponseCallAdapter( + getParameterUpperBound( + 0, + responseType + ) + ) + } else { + BodyCallAdapter(responseType) + } + } + + private class BodyCallAdapter( + private val responseType: Type + ) : CallAdapter> { + + override fun responseType() = responseType + + override fun adapt(call: Call): Deferred { + val deferred = CompletableDeferred() + + deferred.invokeOnCompletion { + if (deferred.isCancelled) { + call.cancel() + } + } + + call.enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + deferred.completeExceptionally(t) + } + + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + deferred.complete(response.body()!!) + } else { + deferred.completeExceptionally(HttpException(response)) + } + } + }) + + return deferred + } + } + + private class ResponseCallAdapter( + private val responseType: Type + ) : CallAdapter>> { + + override fun responseType() = responseType + + override fun adapt(call: Call): Deferred> { + val deferred = CompletableDeferred>() + + deferred.invokeOnCompletion { + if (deferred.isCancelled) { + call.cancel() + } + } + + call.enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + deferred.completeExceptionally(t) + } + + override fun onResponse(call: Call, response: Response) { + deferred.complete(response) + } + }) + + return deferred + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt new file mode 100644 index 000000000..1ef3b7d7b --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -0,0 +1,49 @@ +package io.legado.app.help.http + +import okhttp3.* +import java.util.* +import java.util.concurrent.TimeUnit + +object HttpHelper { + + val client: OkHttpClient = getOkHttpClient() + + + private fun getOkHttpClient(): OkHttpClient { + val cs = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2) + .build() + + val specs = ArrayList() + specs.add(cs) + specs.add(ConnectionSpec.COMPATIBLE_TLS) + specs.add(ConnectionSpec.CLEARTEXT) + + val sslParams = SSLHelper.getSslSocketFactory() + return OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager) + .hostnameVerifier(SSLHelper.unsafeHostnameVerifier) + .connectionSpecs(specs) + .followRedirects(true) + .followSslRedirects(true) + .protocols(listOf(Protocol.HTTP_1_1)) + .addInterceptor(getHeaderInterceptor()) + .build() + } + + private fun getHeaderInterceptor(): Interceptor { + return Interceptor { chain -> + val request = chain.request() + .newBuilder() + .addHeader("Keep-Alive", "300") + .addHeader("Connection", "Keep-Alive") + .addHeader("Cache-Control", "no-cache") + .build() + chain.proceed(request) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/SSLHelper.kt b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt new file mode 100644 index 000000000..dcf5b7709 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt @@ -0,0 +1,175 @@ +package io.legado.app.help.http + +import javax.net.ssl.* +import java.io.IOException +import java.io.InputStream +import java.security.KeyManagementException +import java.security.KeyStore +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +object SSLHelper { + + val sslSocketFactory: SSLParams + get() = getSslSocketFactoryBase(null, null, null) + + /** + * 为了解决客户端不信任服务器数字证书的问题,网络上大部分的解决方案都是让客户端不对证书做任何检查, + * 这是一种有很大安全漏洞的办法 + */ + val unsafeTrustManager: X509TrustManager = object : X509TrustManager { + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + } + + override fun getAcceptedIssuers(): Array { + return arrayOf() + } + } + + /** + * 此类是用于主机名验证的基接口。 在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配, + * 则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。策略可以是基于证书的或依赖于其他验证方案。 + * 当验证 URL 主机名使用的默认规则失败时使用这些回调。如果主机名是可接受的,则返回 true + */ + val unsafeHostnameVerifier: HostnameVerifier = HostnameVerifier { _, _ -> true } + + class SSLParams { + var sSLSocketFactory: SSLSocketFactory? = null + var trustManager: X509TrustManager? = null + } + + /** + * https单向认证 + * 可以额外配置信任服务端的证书策略,否则默认是按CA证书去验证的,若不是CA可信任的证书,则无法通过验证 + */ + fun getSslSocketFactory(trustManager: X509TrustManager): SSLParams { + return getSslSocketFactoryBase(trustManager, null, null) + } + + /** + * https单向认证 + * 用含有服务端公钥的证书校验服务端证书 + */ + fun getSslSocketFactory(vararg certificates: InputStream): SSLParams { + return getSslSocketFactoryBase(null, null, null, *certificates) + } + + /** + * https双向认证 + * bksFile 和 password -> 客户端使用bks证书校验服务端证书 + * certificates -> 用含有服务端公钥的证书校验服务端证书 + */ + fun getSslSocketFactory(bksFile: InputStream, password: String, vararg certificates: InputStream): SSLParams { + return getSslSocketFactoryBase(null, bksFile, password, *certificates) + } + + /** + * https双向认证 + * bksFile 和 password -> 客户端使用bks证书校验服务端证书 + * X509TrustManager -> 如果需要自己校验,那么可以自己实现相关校验,如果不需要自己校验,那么传null即可 + */ + fun getSslSocketFactory(bksFile: InputStream, password: String, trustManager: X509TrustManager): SSLParams { + return getSslSocketFactoryBase(trustManager, bksFile, password) + } + + private fun getSslSocketFactoryBase( + trustManager: X509TrustManager?, + bksFile: InputStream?, + password: String?, + vararg certificates: InputStream + ): SSLParams { + val sslParams = SSLParams() + try { + val keyManagers = prepareKeyManager(bksFile, password) + val trustManagers = prepareTrustManager(*certificates) + val manager: X509TrustManager? + manager = //优先使用用户自定义的TrustManager + trustManager ?: if (trustManagers != null) { + //然后使用默认的TrustManager + chooseTrustManager(trustManagers) + } else { + //否则使用不安全的TrustManager + unsafeTrustManager + } + // 创建TLS类型的SSLContext对象, that uses our TrustManager + val sslContext = SSLContext.getInstance("TLS") + // 用上面得到的trustManagers初始化SSLContext,这样sslContext就会信任keyStore中的证书 + // 第一个参数是授权的密钥管理器,用来授权验证,比如授权自签名的证书验证。第二个是被授权的证书管理器,用来验证服务器端的证书 + sslContext.init(keyManagers, manager?.let { arrayOf(it) }, null) + // 通过sslContext获取SSLSocketFactory对象 + sslParams.sSLSocketFactory = sslContext.socketFactory + sslParams.trustManager = manager + return sslParams + } catch (e: NoSuchAlgorithmException) { + throw AssertionError(e) + } catch (e: KeyManagementException) { + throw AssertionError(e) + } + + } + + private fun prepareKeyManager(bksFile: InputStream?, password: String?): Array? { + try { + if (bksFile == null || password == null) return null + val clientKeyStore = KeyStore.getInstance("BKS") + clientKeyStore.load(bksFile, password.toCharArray()) + val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + kmf.init(clientKeyStore, password.toCharArray()) + return kmf.keyManagers + } catch (e: Exception) { + e.printStackTrace() + } + + return null + } + + private fun prepareTrustManager(vararg certificates: InputStream): Array? { + if (certificates.isEmpty()) return null + try { + val certificateFactory = CertificateFactory.getInstance("X.509") + // 创建一个默认类型的KeyStore,存储我们信任的证书 + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + keyStore.load(null) + var index = 0 + for (certStream in certificates) { + val certificateAlias = Integer.toString(index++) + // 证书工厂根据证书文件的流生成证书 cert + val cert = certificateFactory.generateCertificate(certStream) + // 将 cert 作为可信证书放入到keyStore中 + keyStore.setCertificateEntry(certificateAlias, cert) + try { + certStream?.close() + } catch (e: IOException) { + e.printStackTrace() + } + + } + //我们创建一个默认类型的TrustManagerFactory + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + //用我们之前的keyStore实例初始化TrustManagerFactory,这样tmf就会信任keyStore中的证书 + tmf.init(keyStore) + //通过tmf获取TrustManager数组,TrustManager也会信任keyStore中的证书 + return tmf.trustManagers + } catch (e: Exception) { + e.printStackTrace() + } + + return null + } + + private fun chooseTrustManager(trustManagers: Array): X509TrustManager? { + for (trustManager in trustManagers) { + if (trustManager is X509TrustManager) { + return trustManager + } + } + return null + } +} diff --git a/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt index a9a847a5b..fb7b213f7 100644 --- a/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt @@ -1,6 +1,18 @@ package io.legado.app.ui.search import android.app.Application -import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.data.entities.SearchBook -class SearchViewModel(application: Application) : AndroidViewModel(application) +class SearchViewModel(application: Application) : BaseViewModel(application){ + + val searchBooks: LiveData> = MutableLiveData() + + public fun search(){ + + + } + +} diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index da4d4a9c6..9e2a9e07c 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -2,7 +2,6 @@ -