diff --git a/app/build.gradle b/app/build.gradle index c8d349b..d179dd0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,11 +68,7 @@ dependencies { // Retrofit2 implementation "com.squareup.retrofit2:retrofit:${retrofitVersion}" implementation "com.squareup.retrofit2:converter-gson:${retrofitVersion}" - implementation "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}" - - // RxJava2 - implementation "io.reactivex.rxjava2:rxandroid:2.1.0" - implementation "io.reactivex.rxjava2:rxjava:2.2.5" + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' // Glide implementation "pl.droidsonroids.gif:android-gif-drawable:1.2.10" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 30e1c20..af4bfb9 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -58,10 +58,20 @@ # Retain declared checked exceptions for use by a Proxy instance. -keepattributes Exceptions - +#OkHttp -keepattributes Signature -keepattributes Annotation -keep class okhttp3.** { *; } -keep interface okhttp3.** { *; } -dontwarn okhttp3.** --dontwarn okio.** \ No newline at end of file +-dontwarn okio.** + +#Coroutines +# ServiceLoader support + -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} + +# Most of volatile fields are updated with AFU and should not be mangled +-keepclassmembernames class kotlinx.** { + volatile ; +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5d08a75..36f25c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.haretskiy.pavel.gifrandom"> + LiveData.distinctUntilChanged(): LiveData { + val mutableLiveData: MediatorLiveData = MediatorLiveData() + var latestValue: T? = null + mutableLiveData.addSource(this) { + if (latestValue != it) { + mutableLiveData.value = it + latestValue = it + } + } + return mutableLiveData +} + +fun LiveData.map(function: MapperFunction): LiveData { + return Transformations.map(this, function) +} + +typealias MapperFunction = (T) -> O \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/DetailActivity.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/DetailActivity.kt index 86d2778..a2c84f8 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/DetailActivity.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/DetailActivity.kt @@ -12,28 +12,31 @@ import org.koin.android.viewmodel.ext.android.viewModel import org.koin.core.parameter.ParameterList class DetailActivity : AppCompatActivity() { - + private var url = EMPTY_STRING - + private val handler = Handler() + private val detailViewModel: DetailViewModel by viewModel { ParameterList(url) } - + private val binding: ActivityDetailBinding by lazy { DataBindingUtil.setContentView(this, R.layout.activity_detail) as ActivityDetailBinding } - + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + url = intent.getStringExtra(BUNDLE_KEY_URL_DETAIL) - + binding.detailModel = detailViewModel detailViewModel.initObservers(this) - + makeTransition() } - + private fun makeTransition() { ViewCompat.setTransitionName(binding.imageView, VIEW_NAME_IMAGE) - Handler().postDelayed({ recreate() }, START_ANIMATION_DELAY) + handler.postDelayed({ + detailViewModel.invalidate() + }, START_ANIMATION_DELAY) } } diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt index 96566b9..e176726 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/activities/MainActivity.kt @@ -9,6 +9,7 @@ import com.haretskiy.pavel.gifrandom.R import com.haretskiy.pavel.gifrandom.ZERO import com.haretskiy.pavel.gifrandom.adapters.GifAdapter import com.haretskiy.pavel.gifrandom.databinding.ActivityMainBinding +import com.haretskiy.pavel.gifrandom.utils.Toaster import com.haretskiy.pavel.gifrandom.viewModels.MainViewModel import kotlinx.android.synthetic.main.main_content.* import kotlinx.android.synthetic.main.toolbar.* @@ -16,25 +17,27 @@ import org.koin.android.ext.android.inject import org.koin.android.viewmodel.ext.android.viewModel class MainActivity : AppCompatActivity() { - + private val mainViewModel: MainViewModel by viewModel() - + private val adapter: GifAdapter by inject() - + + private val toaster: Toaster by inject() + private val binding: ActivityMainBinding by lazy { DataBindingUtil.setContentView(this, R.layout.activity_main) as ActivityMainBinding } - + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + binding.model = mainViewModel mainViewModel.setBinding(binding) - + initRecyclerView() initToolbar() } - + private fun initRecyclerView() { mainViewModel.pagedListLiveData.observe(this, Observer> { urls -> adapter.submitList(urls) @@ -44,11 +47,16 @@ class MainActivity : AppCompatActivity() { rv_gifs.scrollToPosition(ZERO) } }) + mainViewModel.observeOnline() + .observe(this, Observer { + toaster.showToast(if (it == true) getString(R.string.online_state) else getString( + R.string.offline_state)) + }) rv_gifs.adapter = adapter } - + private fun initToolbar() { setSupportActionBar(toolbar) } - + } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/adapters/GifAdapter.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/adapters/GifAdapter.kt index ea996e4..5f3d0ac 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/adapters/GifAdapter.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/adapters/GifAdapter.kt @@ -8,12 +8,11 @@ import android.view.ViewGroup import com.haretskiy.pavel.gifrandom.EMPTY_STRING import com.haretskiy.pavel.gifrandom.R import com.haretskiy.pavel.gifrandom.databinding.ItemHolderBinding +import com.haretskiy.pavel.gifrandom.pagging.DiffCallBack import com.haretskiy.pavel.gifrandom.utils.ImageLoader import com.haretskiy.pavel.gifrandom.utils.Router -import com.haretskiy.pavel.gifrandom.utils.pagging.DiffCallBack import com.haretskiy.pavel.gifrandom.views.GifHolder - class GifAdapter(diffCallback: DiffCallBack, private val imageLoader: ImageLoader, private val router: Router) : PagedListAdapter(diffCallback) { diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/Repository.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/Repository.kt index 62352ab..05e1c4b 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/Repository.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/Repository.kt @@ -1,9 +1,7 @@ package com.haretskiy.pavel.gifrandom.data interface Repository { -// fun loadTrendingGifs(rating: String, offset: String = ZERO_OFFSET): Observable> -// fun loadGifsByWord(word: String, rating: String, offset: String = ZERO_OFFSET): Observable> - fun loadTrendingGifs(rating: String, offset: String, resultCallback: RepositoryImpl.ResultCallback) - fun loadGifsByWord(word: String, rating: String, offset: String, resultCallback: RepositoryImpl.ResultCallback) + fun loadTrendingGifs(rating: String, offset: String) : List + fun loadGifsByWord(word: String, rating: String, offset: String) : List } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt index 827b19c..d8e671d 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/data/RepositoryImpl.kt @@ -1,44 +1,44 @@ package com.haretskiy.pavel.gifrandom.data +import android.util.Log import com.haretskiy.pavel.gifrandom.EMPTY_STRING import com.haretskiy.pavel.gifrandom.models.GifResponse import com.haretskiy.pavel.gifrandom.rest.RestApiImpl -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.* +import java.net.ConnectException -class RepositoryImpl( - private val restApi: RestApiImpl) : Repository { - - override fun loadTrendingGifs(rating: String, offset: String, resultCallback: ResultCallback) { - load(restApi.loadGifs(rating, offset), resultCallback) - } - - override fun loadGifsByWord(word: String, rating: String, offset: String, resultCallback: ResultCallback) { - load(restApi.loadGifsByWord(word, rating, offset), resultCallback) +class RepositoryImpl(private val restApi: RestApiImpl) : Repository { + + override fun loadTrendingGifs(rating: String, offset: String) = load(restApi.loadGifsAsync( + rating, + offset)) + + override fun loadGifsByWord(word: String, + rating: String, + offset: String) = load(restApi.loadGifsByWordAsync(word, + rating, + offset)) + + private val handler = CoroutineExceptionHandler { _, exception -> + Log.d("RepositoryImpl", "Caught $exception") + exception.printStackTrace() } - - private fun load(obs: Observable, resultCallback: ResultCallback) { - obs.subscribeOn(Schedulers.io()) - .map { - it.data.map { - it.images?.original?.url ?: EMPTY_STRING - } - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - if (it != null) { - resultCallback.onResult(it) - } - }, - { - resultCallback.onResult(emptyList()) - }) + + private fun load(deferred: Deferred): List = runBlocking(Dispatchers.Default + handler) { + try { + val responseData = deferred.await() + Log.d("RepositoryImpl", "On success") + convertDataAsync(responseData).await() + } catch (ex: ConnectException) { + Log.d("RepositoryImpl", "On error$ex") + emptyList() + } } - - interface ResultCallback { - fun onResult(list: List) + + private fun convertDataAsync(responseData: GifResponse) = GlobalScope.async(Dispatchers.Default + handler) { + responseData.data.map { + it.images?.original?.url ?: EMPTY_STRING + } } - + } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt index 4ffe446..9433382 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/di/Modules.kt @@ -1,32 +1,31 @@ package com.haretskiy.pavel.gifrandom.di +import android.content.Context +import android.net.ConnectivityManager import com.google.gson.GsonBuilder import com.haretskiy.pavel.gifrandom.BASE_URL import com.haretskiy.pavel.gifrandom.adapters.GifAdapter import com.haretskiy.pavel.gifrandom.data.Repository import com.haretskiy.pavel.gifrandom.data.RepositoryImpl -import com.haretskiy.pavel.gifrandom.rest.JsonInterceptor +import com.haretskiy.pavel.gifrandom.pagging.DiffCallBack +import com.haretskiy.pavel.gifrandom.pagging.GifsSourceFactory import com.haretskiy.pavel.gifrandom.rest.RestApi import com.haretskiy.pavel.gifrandom.rest.RestApiImpl import com.haretskiy.pavel.gifrandom.utils.* -import com.haretskiy.pavel.gifrandom.utils.pagging.DiffCallBack -import com.haretskiy.pavel.gifrandom.utils.pagging.GifsSourceFactory import com.haretskiy.pavel.gifrandom.viewModels.DetailViewModel import com.haretskiy.pavel.gifrandom.viewModels.MainViewModel +import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory import okhttp3.OkHttpClient import org.koin.android.ext.koin.androidApplication import org.koin.android.viewmodel.ext.koin.viewModel import org.koin.dsl.module.Module import org.koin.dsl.module.module import retrofit2.Retrofit -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory -val restModule: Module = module(definition = { +val restModule: Module = module { single { - OkHttpClient.Builder() - .addInterceptor(JsonInterceptor()) - .build() + OkHttpClient.Builder().build() } single { GsonBuilder().setLenient() @@ -37,29 +36,34 @@ val restModule: Module = module(definition = { .baseUrl(BASE_URL) .client(get()) .addConverterFactory(GsonConverterFactory.create(get())) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addCallAdapterFactory(CoroutineCallAdapterFactory()) .build() .create(RestApi::class.java) } single { RestApiImpl(get()) } -}) + + single { Connectivity(androidApplication(), get()) } + + factory { androidApplication().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } +} -val appModule: Module = module(definition = { +val appModule: Module = module { single { Toaster(androidApplication()) } single { ImageLoaderImpl() as ImageLoader } single { RouterImpl(androidApplication()) as Router } single { RepositoryImpl(get()) as Repository } single { DiffCallBack() } single { GifsSourceFactory(get()) } + factory { GifAdapter(get(), get(), get()) } -}) +} -val viewModelModule: Module = module(definition = { - viewModel { MainViewModel(androidApplication(), get()) } - viewModel {parameterList -> +val viewModelModule: Module = module { + viewModel { MainViewModel(androidApplication(), get(), get()) } + viewModel { parameterList -> DetailViewModel(get(), parameterList[0]) } -}) +} val modules = listOf(restModule, appModule, viewModelModule) diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/DiffCalback.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/DiffCalback.kt similarity index 85% rename from app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/DiffCalback.kt rename to app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/DiffCalback.kt index 3e63d02..0d74d6e 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/DiffCalback.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/DiffCalback.kt @@ -1,4 +1,4 @@ -package com.haretskiy.pavel.gifrandom.utils.pagging +package com.haretskiy.pavel.gifrandom.pagging import android.support.v7.util.DiffUtil diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsDataSource.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsDataSource.kt new file mode 100644 index 0000000..b545c57 --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsDataSource.kt @@ -0,0 +1,59 @@ +package com.haretskiy.pavel.gifrandom.pagging + +import android.arch.paging.PositionalDataSource +import com.haretskiy.pavel.gifrandom.ZERO_OFFSET +import com.haretskiy.pavel.gifrandom.data.Repository + +class GifsDataSource(private val repository: Repository, + private var gifsLoadedCallback: GifsLoadedCallback, + private var rating: String, + private var word: String) : PositionalDataSource() { + + override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { + gifsLoadedCallback.onStartPageLoad() + when { + word.isEmpty() -> handleLoadRange(repository.loadTrendingGifs(rating, + params.startPosition.toString()), callback) + else -> handleLoadRange(repository.loadGifsByWord(word, + rating, + params.startPosition.toString()), callback) + } + } + + private fun handleLoadRange(listOfGifs: List, + callback: LoadRangeCallback) { + callback.onResult(listOfGifs) + gifsLoadedCallback.onFinishPageLoad() + } + + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + gifsLoadedCallback.onStartInitialLoad() + when { + word.isEmpty() -> handleLoadInitial(repository.loadTrendingGifs(rating, ZERO_OFFSET), + params, + callback) + else -> handleLoadInitial(repository.loadGifsByWord(word, rating, ZERO_OFFSET), + params, + callback) + } + } + + private fun handleLoadInitial(listOfGifs: List, + params: LoadInitialParams, + callback: LoadInitialCallback) { + var startPos = params.requestedStartPosition + when { + startPos < 0 -> startPos = 0 + } + callback.onResult(listOfGifs, startPos) + gifsLoadedCallback.onFinishInitialLoad() + + } + + interface GifsLoadedCallback { + fun onStartInitialLoad() {} + fun onFinishInitialLoad() {} + fun onStartPageLoad() {} + fun onFinishPageLoad() {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsSourceFactory.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsSourceFactory.kt similarity index 94% rename from app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsSourceFactory.kt rename to app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsSourceFactory.kt index 91eb43b..a50f560 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsSourceFactory.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/pagging/GifsSourceFactory.kt @@ -1,4 +1,4 @@ -package com.haretskiy.pavel.gifrandom.utils.pagging +package com.haretskiy.pavel.gifrandom.pagging import android.arch.paging.DataSource import com.haretskiy.pavel.gifrandom.DEFAULT_RATING diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt deleted file mode 100644 index 5df51a0..0000000 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/JsonInterceptor.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.haretskiy.pavel.gifrandom.rest - -import android.util.Log -import com.haretskiy.pavel.gifrandom.* -import okhttp3.Interceptor -import okhttp3.Response -import okhttp3.ResponseBody - - -class JsonInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response? { - val request = chain.request() - try { - val t1 = System.nanoTime() - - val url = request.url().newBuilder().addQueryParameter("apikey", API_KEY).build() - val requestBuilder = request.newBuilder().url(url) - val newRequest = requestBuilder.build() - - Log.d(START, "Sending request ${newRequest.url()} Headers: ${newRequest.headers()}") - - val response = chain.proceed(newRequest) - - val responseBodyString = response.body()?.string() ?: EMPTY_STRING - - val t2 = System.nanoTime() - - val prettyString = responseBodyString.toPrettyFormat() - val length = prettyString.length - if (length > 4000) { - val chunkCount = length / 4000 - for (i in 0..chunkCount) { - val max = 4000 * (i + 1) - if (max >= length) { - Log.d(RESPONSE, "chunk $i of $chunkCount:") - Log.e(RESPONSE, prettyString.substring(4000 * i)) - } else { - Log.d(RESPONSE, "chunk $i of $chunkCount:") - Log.e(RESPONSE, prettyString.substring(4000 * i, max)) - } - } - } else { - Log.e(RESPONSE, prettyString) - } - - Log.d(END, "Received response for ${response.request().url()} for ${(t2 - t1) / 1e6} milliseconds ") - - val responseBody = response.body() - - return response.newBuilder().body(ResponseBody.create(responseBody?.contentType(), responseBodyString.toByteArray())).build() - } catch (ex: Exception) { - return Response.Builder().build() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt index 9baaf7d..a4f1913 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApi.kt @@ -2,21 +2,17 @@ package com.haretskiy.pavel.gifrandom.rest import com.haretskiy.pavel.gifrandom.ZERO_OFFSET import com.haretskiy.pavel.gifrandom.models.GifResponse -import io.reactivex.Observable +import kotlinx.coroutines.Deferred import retrofit2.http.GET import retrofit2.http.Query - interface RestApi { - + @GET("gifs/trending") - fun loadGifs(@Query("limit") limit: Int, - @Query("rating") rating: String, - @Query("offset") offset: String = ZERO_OFFSET): Observable - + fun loadGifsAsync(@Query("apikey") apikey: String, @Query("limit") limit: Int, @Query("rating") rating: String, @Query( + "offset") offset: String = ZERO_OFFSET): Deferred + @GET("gifs/search") - fun loadGifsBySearchWord(@Query("q") searchWord: String, - @Query("limit") limit: Int, - @Query("rating") rating: String, - @Query("offset") offset: String = ZERO_OFFSET): Observable + fun loadGifsBySearchWordAsync(@Query("apikey") apikey: String, @Query("q") searchWord: String, @Query( + "limit") limit: Int, @Query("rating") rating: String, @Query("offset") offset: String = ZERO_OFFSET): Deferred } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt index e635f34..560346a 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/rest/RestApiImpl.kt @@ -1,12 +1,13 @@ package com.haretskiy.pavel.gifrandom.rest -import com.haretskiy.pavel.gifrandom.LIMIT +import com.haretskiy.pavel.gifrandom.API_KEY +import com.haretskiy.pavel.gifrandom.PAGE_SIZE import com.haretskiy.pavel.gifrandom.ZERO_OFFSET class RestApiImpl(private val restApi: RestApi) { - fun loadGifs(rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifs(LIMIT, rating, offset) + fun loadGifsAsync(rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsAsync(API_KEY, PAGE_SIZE, rating, offset) - fun loadGifsByWord(word: String, rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsBySearchWord(word, LIMIT, rating, offset) + fun loadGifsByWordAsync(word: String, rating: String, offset: String = ZERO_OFFSET) = restApi.loadGifsBySearchWordAsync(API_KEY, word, PAGE_SIZE, rating, offset) } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ActivityCounter.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ActivityCounter.kt new file mode 100644 index 0000000..60350d5 --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ActivityCounter.kt @@ -0,0 +1,103 @@ +package com.haretskiy.pavel.gifrandom.utils + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import android.util.Log +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.actor +import kotlin.coroutines.CoroutineContext + +object ActivityCounter : CoroutineScope { + + private val job = Job() + + private val actionStore = HashMap, (Count) -> Unit>() + + private val handler = CoroutineExceptionHandler { _, exception -> + Log.e("Error", "Caught $exception") + exception.printStackTrace() + } + + override val coroutineContext: CoroutineContext + get() = job + handler + + private var counter: Int = 0 + + private val connectActor = actor(Dispatchers.Default) { + var prevCount: Count? = null + for (count in channel) { + if (count != prevCount) { + for ((_, action) in actionStore) { + GlobalScope.launch(Dispatchers.Default) { + action.invoke(count) + } + } + prevCount = count + + Log.d("ActivityCounter", + "Activities: old count =${count.previousValue}, new count =${count.newValue}") + } + } + } + + fun addAction(clazz: Class, action: (count: Count) -> Unit) { + actionStore[clazz] = action + } + + fun removeAction(clazz: Class) { + actionStore.remove(clazz) + } + + fun init(app: Application) { + app.registerActivityLifecycleCallbacks(object : SimpleActivityLifecycleCallbacks() { + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + + val count = Count() + count.previousValue = counter + + counter++ + Log.d("ActivityCounter", "onActivityCreated ${activity.javaClass.name} ") + + count.newValue = counter + + GlobalScope.launch { + connectActor.send(count) + } + } + + override fun onActivityDestroyed(activity: Activity) { + + val count = Count() + count.previousValue = counter + + counter-- + Log.d("ActivityCounter", "onActivityDestroyed ${activity.javaClass.name} ") + + count.newValue = counter + GlobalScope.launch { + connectActor.send(count) + } + if (count.newValue == 0) { + Log.d("ActivityCounter", "0 activities, Cancel job") + } + } + }) + } + + class Count { + var newValue: Int = 0 + var previousValue: Int = 0 + + override fun equals(other: Any?): Boolean { + return (other is Count) && other.newValue == newValue && other.previousValue == previousValue + } + + override fun hashCode(): Int { + var result = newValue + result = 31 * result + previousValue + return result + } + } +} diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt new file mode 100644 index 0000000..380fa8b --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/Connectivity.kt @@ -0,0 +1,98 @@ +package com.haretskiy.pavel.gifrandom.utils + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.util.Log +import com.haretskiy.pavel.gifrandom.distinctUntilChanged +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.actor +import kotlin.coroutines.CoroutineContext + +class Connectivity(context: Context, private val manager: ConnectivityManager) : CoroutineScope { + + private var isStarted = false + + private val job: Job = Job() + + override val coroutineContext: CoroutineContext + get() = job + + private val onlineData = MutableLiveData() + + private val handler = CoroutineExceptionHandler { _, exception -> + Log.e("Error", "Caught $exception") + exception.printStackTrace() + } + + private val connectActor = actor(Dispatchers.Default + handler) { + for (intents in channel) { + onlineData.postValue(isOnlineAsync()) + } + } + + fun start() { + if (!isStarted) { + connectivityListener.start() + isStarted = true + } + } + + fun stop() { + if (isStarted) { + connectivityListener.stop() + isStarted = false + job.cancel() + } + } + + private val connectivityListener = ConnectivityListener(context, + connectActor, + IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION), + null, + null) + + private fun isOnline(): Boolean { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + val activeNetworks = manager.allNetworks + for (network in activeNetworks) { + val networkInfo = manager.getNetworkInfo(network) + if (networkInfo != null && networkInfo.isConnected) { + Log.d(TAG, "isOnline true: $networkInfo") + return true + } + } + } else { + val networksInfo = this.manager.allNetworkInfo + if (networksInfo != null) for (networkInfo in networksInfo) { + if (networkInfo != null && networkInfo.state == NetworkInfo.State.CONNECTED) { + Log.d(TAG, "isOnline true: $networkInfo.toString()") + return true + } + } + } + + Log.d(TAG, "isOnline false") + + return false + } + + private fun isOnlineAsync(): Boolean { + return runBlocking { + withContext(Dispatchers.Default + handler) { isOnline() } + } + } + + fun onlineChanges(): LiveData { + return onlineData.distinctUntilChanged() + } + + companion object { + const val TAG = "Connectivity" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityListener.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityListener.kt new file mode 100644 index 0000000..818f0fc --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/ConnectivityListener.kt @@ -0,0 +1,47 @@ +package com.haretskiy.pavel.gifrandom.utils + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Handler +import android.util.Log +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.launch + +class ConnectivityListener(private val context: Context, + private val actor: SendChannel, + private val intentFilter: IntentFilter, + private val broadcastPermission: String?, + private val schedulerHandler: Handler?) { + + private lateinit var broadcastReceiver: BroadcastReceiver + + private val handler = CoroutineExceptionHandler { _, exception -> + Log.e("Error", "Caught $exception") + exception.printStackTrace() + } + + fun start() { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + GlobalScope.launch(handler) { + actor.send(intent) + } + } + } + + context.registerReceiver(broadcastReceiver, + intentFilter, + broadcastPermission, + schedulerHandler) + } + + fun stop() { + GlobalScope.launch(handler) { + context.unregisterReceiver(broadcastReceiver) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/SimpleActivityLifecycleCallbacks.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/SimpleActivityLifecycleCallbacks.kt new file mode 100644 index 0000000..55ce101 --- /dev/null +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/SimpleActivityLifecycleCallbacks.kt @@ -0,0 +1,29 @@ +package com.haretskiy.pavel.gifrandom.utils + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +open class SimpleActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks { + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + + override fun onActivityStarted(activity: Activity) { + } + + override fun onActivityResumed(activity: Activity) { + } + + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityStopped(activity: Activity) { + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) { + } + + override fun onActivityDestroyed(activity: Activity) { + } +} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt deleted file mode 100644 index 8bfabcf..0000000 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/utils/pagging/GifsDataSource.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.haretskiy.pavel.gifrandom.utils.pagging - -import android.arch.paging.PositionalDataSource -import com.haretskiy.pavel.gifrandom.ZERO_OFFSET -import com.haretskiy.pavel.gifrandom.data.Repository -import com.haretskiy.pavel.gifrandom.data.RepositoryImpl.ResultCallback - -class GifsDataSource( - private val repository: Repository, - private var gifsLoadedCallback: GifsLoadedCallback, - private var rating: String, - private var word: String) : PositionalDataSource() { - - override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { - gifsLoadedCallback.onStartPageLoad() - when { - word.isEmpty() -> repository.loadTrendingGifs(rating, params.startPosition.toString(), resultCallback(callback)) - else -> repository.loadGifsByWord(word, rating, params.startPosition.toString(), resultCallback(callback)) - } - } - - override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { - gifsLoadedCallback.onStartInitialLoad() - when { - word.isEmpty() -> repository.loadTrendingGifs(rating, ZERO_OFFSET, initialResultCallback(params, callback)) - else -> repository.loadGifsByWord(word, rating, ZERO_OFFSET, initialResultCallback(params, callback)) - } - } - - private fun initialResultCallback(params: LoadInitialParams, callback: LoadInitialCallback) = object : ResultCallback { - override fun onResult(list: List) = try { - var startPos = params.requestedStartPosition - when { - startPos < 0 -> startPos = 0 - } - callback.onResult(list, startPos) - gifsLoadedCallback.onFinishInitialLoad() - } catch (ex: Exception) { - ex.printStackTrace() - } - } - - private fun resultCallback(callback: LoadRangeCallback) = object : ResultCallback { - override fun onResult(list: List) { - callback.onResult(list) - gifsLoadedCallback.onFinishPageLoad() - } - } - - interface GifsLoadedCallback { - fun onStartInitialLoad() {} - fun onFinishInitialLoad() {} - fun onStartPageLoad() {} - fun onFinishPageLoad() {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/DetailViewModel.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/DetailViewModel.kt index 3d9c097..b089b06 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/DetailViewModel.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/DetailViewModel.kt @@ -25,4 +25,8 @@ class DetailViewModel(imageLoader: ImageLoader, progress.set(if (it == true) View.VISIBLE else View.GONE) }) } + + fun invalidate() { + url.notifyChange() + } } \ No newline at end of file diff --git a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt index b34e47b..dc935cc 100644 --- a/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt +++ b/app/src/main/java/com/haretskiy/pavel/gifrandom/viewModels/MainViewModel.kt @@ -11,61 +11,72 @@ import android.view.View import android.widget.AdapterView import com.haretskiy.pavel.gifrandom.* import com.haretskiy.pavel.gifrandom.databinding.ActivityMainBinding -import com.haretskiy.pavel.gifrandom.utils.pagging.GifsDataSource -import com.haretskiy.pavel.gifrandom.utils.pagging.GifsSourceFactory +import com.haretskiy.pavel.gifrandom.pagging.GifsDataSource +import com.haretskiy.pavel.gifrandom.pagging.GifsSourceFactory +import com.haretskiy.pavel.gifrandom.utils.Connectivity import java.util.concurrent.Executors - class MainViewModel(private val context: Application, - private val factory: GifsSourceFactory) : AndroidViewModel(context) { - + private val factory: GifsSourceFactory, + private val connectivity: Connectivity) : AndroidViewModel(context) { + private val config = PagedList.Config.Builder() .setEnablePlaceholders(false) .setInitialLoadSizeHint(INITIAL_LOAD_SIZE) .setPrefetchDistance(PREFETCH_SIZE) - .setPageSize(LIMIT) + .setPageSize(PAGE_SIZE) .build() - - val pagedListLiveData = LivePagedListBuilder( - factory.initCallback(object : GifsDataSource.GifsLoadedCallback { + + val pagedListLiveData = + LivePagedListBuilder(factory.initCallback(object : GifsDataSource.GifsLoadedCallback { override fun onStartInitialLoad() { progress.set(View.VISIBLE) } - + override fun onFinishInitialLoad() { progress.set(View.GONE) } - }), config) - .setFetchExecutor(Executors.newSingleThreadExecutor()) - .build() - + }), config).setFetchExecutor(Executors.newSingleThreadExecutor()) + .build() + val searchWord: ObservableField = ObservableField() val ratingSelectedPos = ObservableInt(ZERO) val progress = ObservableInt(View.VISIBLE) val toolbarVisibility = ObservableInt(View.VISIBLE) - + val positLiveData = MutableLiveData() - + + init { + connectivity.start() + } + + override fun onCleared() { + connectivity.stop() + super.onCleared() + } + private fun getCurrentRating(): String { val ratings = context.resources.getStringArray(R.array.ratings) return ratings[ratingSelectedPos.get()] } - + fun onClickSearch(@Suppress("UNUSED_PARAMETER") v: View) { factory.rating = getCurrentRating() factory.word = searchWord.get() ?: EMPTY_STRING factory.invalidate() } - + fun onClickFilter(@Suppress("UNUSED_PARAMETER") v: View) { positLiveData.postValue(true) } - - + + fun observeOnline() = connectivity.onlineChanges() + @Suppress("UNUSED_PARAMETER") fun onRatingSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + //not implemented } - + fun setBinding(binding: ActivityMainBinding) { binding.appbar.addOnOffsetChangedListener { appBarLayout, verticalOffset -> if (Math.abs(verticalOffset) - appBarLayout.totalScrollRange >= 0) { @@ -77,6 +88,6 @@ class MainViewModel(private val context: Application, } } } - + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae936e6..d955ed0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,8 @@ GifRandom Search + Online + Offline Y G diff --git a/build.gradle b/build.gradle index f2aa1a9..1dad410 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.20' + ext.kotlin_version = '1.3.21' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.android.tools.build:gradle:3.3.0' } }