diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 45b5654..3cc336b 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,9 +1,22 @@ - - + + diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 79ee123..6e6eec1 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 37a7509..d5d35ec 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 775a2a9..54b4d46 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,34 +4,82 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + android { compileSdkVersion 29 - buildToolsVersion "29.0.2" + buildToolsVersion "29.0.3" defaultConfig { applicationId "com.terebenin.durov_return_the_wall" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 29 versionCode 1 - versionName "1.0" + versionName "v0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "REDIRECT_URI", "\"https://oauth.vk.com/blank.html\"") + buildConfigField("String", "AUTHORIZE_URI", "\"https://oauth.vk.com/authorize\"") + buildConfigField("String", "SCOPE", "\"friends,offline,wall\"") + buildConfigField("String", "API_VERSION", "\"5.103\"") + buildConfigField("String", "RESPONSE_TYPE", "\"token\"") + buildConfigField("String", "DISPLAY_TYPE", "\"page\"") + buildConfigField("String", "APP_ID", "\"7312625\"") + buildConfigField("String", "BASE_API_URL", "\"https://api.vk.com/\"") + } + buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + + dataBinding { + enabled = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.core:core-ktx:1.0.2' + + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" + implementation "androidx.fragment:fragment-ktx:1.2.5" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "com.google.android.material:material:1.1.0" + // alternately - if using Java8, use the following instead of lifecycle-compiler + + implementation "com.squareup.retrofit2:retrofit:2.8.2" + implementation "com.squareup.retrofit2:converter-gson:2.6.1" + implementation 'com.squareup.okhttp3:logging-interceptor:4.7.2' + implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2" + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" + + kapt 'com.android.databinding:compiler:3.1.4' + + implementation 'com.github.bumptech.glide:glide:4.11.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' + + implementation "com.jakewharton.threetenabp:threetenabp:1.2.4" + + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } diff --git a/app/src/androidTest/java/com/terebenin/durov_return_the_wall/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/terebenin/durov_return_the_wall/ExampleInstrumentedTest.kt index 9aa69a8..8163504 100644 --- a/app/src/androidTest/java/com/terebenin/durov_return_the_wall/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/terebenin/durov_return_the_wall/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.terebenin.durov_return_the_wall -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b42a2e..91a1c05 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,17 +2,25 @@ + - + + - diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/MainActivity.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/MainActivity.kt deleted file mode 100644 index 27aa4a1..0000000 --- a/app/src/main/java/com/terebenin/durov_return_the_wall/MainActivity.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.terebenin.durov_return_the_wall - -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle -import com.terebenin.durov_return_the_wall.ui.main.MainFragment - -class MainActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.main_activity) - if (savedInstanceState == null) { - supportFragmentManager.beginTransaction() - .replace(R.id.container, MainFragment.newInstance()) - .commitNow() - } - } - -} diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/datasource/network/VkApi.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/datasource/network/VkApi.kt new file mode 100644 index 0000000..7d27a17 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/datasource/network/VkApi.kt @@ -0,0 +1,15 @@ +package com.terebenin.durov_return_the_wall.data.datasource.network + +import com.terebenin.durov_return_the_wall.data.newsfeed.response.NewsfeedResponse +import retrofit2.Response +import retrofit2.http.GET + +interface VkApi { + + companion object { + const val METHOD_PATH = "method" + } + + @GET("$METHOD_PATH/newsfeed.get") + suspend fun getNewsfeed(): Response +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/datasource/network/VkApiFactory.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/datasource/network/VkApiFactory.kt new file mode 100644 index 0000000..80ec8f2 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/datasource/network/VkApiFactory.kt @@ -0,0 +1,49 @@ +package com.terebenin.durov_return_the_wall.data.datasource.network + +import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory +import com.terebenin.durov_return_the_wall.BuildConfig +import com.terebenin.durov_return_the_wall.presentation.global.VkApplication.Companion.prefs +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + + +object VkApiFactory { + + private val authInterceptor = Interceptor { chain -> + val newUrl = chain.request().url + .newBuilder() + .addQueryParameter("access_token", prefs.accessToken.token) + .addQueryParameter("v", BuildConfig.API_VERSION) + .build() + + val newRequest = chain.request() + .newBuilder() + .url(newUrl) + .build() + + chain.proceed(newRequest) + } + + private fun initOkHttpClient(): OkHttpClient { + val okHttpClientBuilder = OkHttpClient.Builder().apply { + addInterceptor(authInterceptor) + val logInterceptor = HttpLoggingInterceptor() + logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY) + addInterceptor(logInterceptor) + } + return okHttpClientBuilder.build() + } + + private fun retrofit(): Retrofit = Retrofit.Builder() + .client(initOkHttpClient()) + .baseUrl(BuildConfig.BASE_API_URL) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(CoroutineCallAdapterFactory()) + .build() + + val vkApi: VkApi = retrofit().create(VkApi::class.java) + +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/datasource/storage/Prefs.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/datasource/storage/Prefs.kt new file mode 100644 index 0000000..2f72fac --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/datasource/storage/Prefs.kt @@ -0,0 +1,31 @@ +package com.terebenin.durov_return_the_wall.data.datasource.storage + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.terebenin.durov_return_the_wall.domain.global.AccessToken + +class Prefs( + private val context: Context, + private val gson: Gson +) { + private fun getSharedPreferences(prefsName: String) = + context.getSharedPreferences(prefsName, Context.MODE_PRIVATE) + + private val AUTH_DATA = "auth_data" + private val ACCESS_TOKEN = "access_token" + private val authPrefs by lazy { getSharedPreferences(AUTH_DATA) } + + private val accessTokenType = object : TypeToken() {}.type + + var accessToken: AccessToken + get() { + return gson.fromJson(authPrefs.getString(ACCESS_TOKEN, ""), accessTokenType) + } + set(value) { + authPrefs.edit().putString(ACCESS_TOKEN, gson.toJson(value)).apply() + + } + + +} diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/NewsfeedRepositoryImpl.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/NewsfeedRepositoryImpl.kt new file mode 100644 index 0000000..e95e123 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/NewsfeedRepositoryImpl.kt @@ -0,0 +1,12 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed + +import com.terebenin.durov_return_the_wall.data.datasource.network.VkApi +import com.terebenin.durov_return_the_wall.data.newsfeed.response.toDomainModel +import com.terebenin.durov_return_the_wall.domain.newsfeed.NewsfeedRepository +import com.terebenin.durov_return_the_wall.domain.newsfeed.model.NewsfeedResponseDomainModel + +class NewsfeedRepositoryImpl(private val vkApi: VkApi) : NewsfeedRepository { + override suspend fun getNewsfeed(): NewsfeedResponseDomainModel? { + return vkApi.getNewsfeed().body()?.response?.toDomainModel() + } +} diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Attachments.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Attachments.kt new file mode 100644 index 0000000..9ff5277 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Attachments.kt @@ -0,0 +1,19 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Attachments( + + @field:SerializedName("photo") + val photo: Photo? = null, + + @field:SerializedName("type") + val type: String? = null, + + @field:SerializedName("video") + val video: Video? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Comments.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Comments.kt new file mode 100644 index 0000000..9b52ca6 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Comments.kt @@ -0,0 +1,16 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import com.google.gson.annotations.SerializedName + + +data class Comments( + + @field:SerializedName("count") + val count: Int? = null, + + @field:SerializedName("groups_can_post") + val groupsCanPost: Boolean? = null, + + @field:SerializedName("can_post") + val canPost: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Group.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Group.kt new file mode 100644 index 0000000..88773bb --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Group.kt @@ -0,0 +1,31 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import com.google.gson.annotations.SerializedName + + +data class Group( + + @field:SerializedName("photo_50") + val photo50: String? = null, + + @field:SerializedName("screen_name") + val screenName: String? = null, + + @field:SerializedName("name") + val name: String? = null, + + @field:SerializedName("id") + val id: Int? = null, + + @field:SerializedName("type") + val type: String? = null, + + @field:SerializedName("photo_100") + val photo100: String? = null, + + @field:SerializedName("photo_200") + val photo200: String? = null, + + @field:SerializedName("is_closed") + val isClosed: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Image.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Image.kt new file mode 100644 index 0000000..3a44344 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Image.kt @@ -0,0 +1,21 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Image( + + @field:SerializedName("width") + val width: Int? = null, + + @field:SerializedName("with_padding") + val withPadding: Int? = null, + + @field:SerializedName("url") + val url: String? = null, + + @field:SerializedName("height") + val height: Int? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Item.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Item.kt new file mode 100644 index 0000000..1631d5a --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Item.kt @@ -0,0 +1,58 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import com.google.gson.annotations.SerializedName + + +data class Item( + + @field:SerializedName("date") + val date: Long? = null, + + @field:SerializedName("attachments") + var attachments: List? = null, + + @field:SerializedName("comments") + val comments: Comments? = null, + + @field:SerializedName("is_favorite") + val isFavorite: Boolean? = null, + + @field:SerializedName("can_set_category") + val canSetCategory: Boolean? = null, + + @field:SerializedName("type") + val type: String? = null, + + @field:SerializedName("can_doubt_category") + val canDoubtCategory: Boolean? = null, + + @field:SerializedName("post_id") + val postId: Int? = null, + + @field:SerializedName("post_source") + val postSource: PostSource? = null, + + @field:SerializedName("marked_as_ads") + val markedAsAds: Int? = null, + + @field:SerializedName("post_type") + val postType: String? = null, + + /** + * Идентификатор источника новости (положительный — новость пользователя, отрицательный — новость группы); + */ + @field:SerializedName("source_id") + val sourceId: Int? = null, + + @field:SerializedName("text") + val text: String? = null, + + @field:SerializedName("reposts") + val reposts: Reposts? = null, + + @field:SerializedName("views") + val views: Views? = null, + + @field:SerializedName("likes") + val likes: Likes? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Likes.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Likes.kt new file mode 100644 index 0000000..f8b1fc4 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Likes.kt @@ -0,0 +1,21 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Likes( + + @field:SerializedName("user_likes") + val userLikes: Int? = null, + + @field:SerializedName("can_publish") + val canPublish: Int? = null, + + @field:SerializedName("can_like") + val canLike: Int? = null, + + @field:SerializedName("count") + val count: Int? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/NewsfeedResponse.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/NewsfeedResponse.kt new file mode 100644 index 0000000..a2269a1 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/NewsfeedResponse.kt @@ -0,0 +1,10 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import com.google.gson.annotations.SerializedName + + +data class NewsfeedResponse( + + @field:SerializedName("response") + val response: Response? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/OnlineInfo.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/OnlineInfo.kt new file mode 100644 index 0000000..7cde430 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/OnlineInfo.kt @@ -0,0 +1,10 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import com.google.gson.annotations.SerializedName + + +data class OnlineInfo( + + @field:SerializedName("visible") + val visible: Boolean? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Photo.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Photo.kt new file mode 100644 index 0000000..ad4bae3 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Photo.kt @@ -0,0 +1,33 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Photo( + + @field:SerializedName("date") + val date: Int? = null, + + @field:SerializedName("sizes") + val sizes: List? = null, + + @field:SerializedName("user_id") + val userId: Int? = null, + + @field:SerializedName("owner_id") + val ownerId: Int? = null, + + @field:SerializedName("access_key") + val accessKey: String? = null, + + @field:SerializedName("album_id") + val albumId: Int? = null, + + @field:SerializedName("id") + val id: Int? = null, + + @field:SerializedName("text") + val text: String? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/PostSource.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/PostSource.kt new file mode 100644 index 0000000..31885fa --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/PostSource.kt @@ -0,0 +1,10 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import com.google.gson.annotations.SerializedName + + +data class PostSource( + + @field:SerializedName("type") + val type: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Profile.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Profile.kt new file mode 100644 index 0000000..5c2ffba --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Profile.kt @@ -0,0 +1,40 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import com.google.gson.annotations.SerializedName + + +data class Profile( + + @field:SerializedName("can_access_closed") + val canAccessClosed: Boolean? = null, + + @field:SerializedName("photo_50") + val photo50: String? = null, + + @field:SerializedName("screen_name") + val screenName: String? = null, + + @field:SerializedName("sex") + val sex: Int? = null, + + @field:SerializedName("last_name") + val lastName: String? = null, + + @field:SerializedName("online") + val online: Int? = null, + + @field:SerializedName("id") + val id: Int? = null, + + @field:SerializedName("photo_100") + val photo100: String? = null, + + @field:SerializedName("first_name") + val firstName: String? = null, + + @field:SerializedName("is_closed") + val isClosed: Boolean? = null, + + @field:SerializedName("online_info") + val onlineInfo: OnlineInfo? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Reposts.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Reposts.kt new file mode 100644 index 0000000..1a2aaad --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Reposts.kt @@ -0,0 +1,13 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import com.google.gson.annotations.SerializedName + + +data class Reposts( + + @field:SerializedName("count") + val count: Int? = null, + + @field:SerializedName("user_reposted") + val userReposted: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Response.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Response.kt new file mode 100644 index 0000000..c4bf22a --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Response.kt @@ -0,0 +1,113 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import com.google.gson.annotations.SerializedName +import com.terebenin.durov_return_the_wall.domain.newsfeed.model.* +import org.threeten.bp.Instant +import org.threeten.bp.ZoneId +import org.threeten.bp.ZonedDateTime +import kotlin.math.abs + + +data class Response( + + @field:SerializedName("next_from") + val nextFrom: String? = null, + + @field:SerializedName("profiles") + val profiles: List? = null, + + @field:SerializedName("groups") + val groups: List? = null, + + @field:SerializedName("items") + val items: List? = null + +) + +internal fun Response.toDomainModel(): NewsfeedResponseDomainModel { + var domainItems: MutableList = mutableListOf() + items?.let { + for (item in items) { + item?.attachments?.let { + if (findOnlyPhotoAttachments(item).isNotEmpty()) { + item.attachments = findOnlyPhotoAttachments(item) + domainItems.add( + createDomainPostItemModel( + this, + item + ) + ) + } + } + } + } + return NewsfeedResponseDomainModel(domainItems) +} + +private fun findOnlyPhotoAttachments( + item: Item +): MutableList { + val attachedPhotos = mutableListOf() + for (i in item.attachments!!) { + if (i?.type == "photo") { + attachedPhotos.add(i) + } + } + return attachedPhotos +} + +fun createDomainPostItemModel(response: Response, item: Item?): PostItemDomainModel { + return PostItemDomainModel( + item?.sourceId, + convertUnixTimeToZonedDateTime(item?.date), + item?.postId, + item?.text, + item?.views, + item?.likes, + getProfileBySourceId(response.profiles, item?.sourceId), + getGroupBySourceId(response.groups, item?.sourceId), + getPostAuthorType(item?.sourceId), + item?.attachments + ) +} + +private fun convertUnixTimeToZonedDateTime(unixTime: Long?): ZonedDateTime { + var i: Instant = Instant.ofEpochSecond(unixTime!!.toLong()) + return ZonedDateTime.ofInstant(i, ZoneId.systemDefault()) +} + + +private fun getPostAuthorType(sourceId: Int?): PostAuthorType? { + return if (sourceId!! > 0) PostAuthorType.User else PostAuthorType.Group +} + +private fun getGroupBySourceId(groups: List?, sourceId: Int?): GroupDomainModel? { + var domainGroupItem: GroupDomainModel? = null + groups?.let { + for (group in groups) { + if (group?.id == abs(sourceId!!)) + domainGroupItem = GroupDomainModel( + group.id, + group.name, + group.photo100 + ) + } + } + return domainGroupItem +} + +private fun getProfileBySourceId(profiles: List?, sourceId: Int?): ProfileDomainModel? { + var domainProfileItem: ProfileDomainModel? = null + profiles?.let { + for (profile in profiles) { + if (profile!!.id == sourceId!!) + domainProfileItem = ProfileDomainModel( + profile.id, + profile.firstName, + profile.lastName, + profile.photo100 + ) + } + } + return domainProfileItem +} diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Size.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Size.kt new file mode 100644 index 0000000..c91d7cc --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Size.kt @@ -0,0 +1,21 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Size( + + @field:SerializedName("width") + val width: Int? = null, + + @field:SerializedName("type") + val type: String? = null, + + @field:SerializedName("url") + val url: String? = null, + + @field:SerializedName("height") + val height: Int? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Video.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Video.kt new file mode 100644 index 0000000..df7b097 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Video.kt @@ -0,0 +1,70 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + + +@Parcelize +data class Video( + + @field:SerializedName("date") + val date: Int? = null, + + @field:SerializedName("image") + val image: List? = null, + + @field:SerializedName("comments") + val comments: Int? = null, + + @field:SerializedName("can_add_to_faves") + val canAddToFaves: Int? = null, + + @field:SerializedName("owner_id") + val ownerId: Int? = null, + + @field:SerializedName("description") + val description: String? = null, + + @field:SerializedName("can_subscribe") + val canSubscribe: Int? = null, + + @field:SerializedName("title") + val title: String? = null, + + @field:SerializedName("type") + val type: String? = null, + + @field:SerializedName("platform") + val platform: String? = null, + + @field:SerializedName("can_repost") + val canRepost: Int? = null, + + @field:SerializedName("duration") + val duration: Int? = null, + + @field:SerializedName("can_comment") + val canComment: Int? = null, + + @field:SerializedName("local_views") + val localViews: Int? = null, + + @field:SerializedName("can_like") + val canLike: Int? = null, + + @field:SerializedName("access_key") + val accessKey: String? = null, + + @field:SerializedName("track_code") + val trackCode: String? = null, + + @field:SerializedName("can_add") + val canAdd: Int? = null, + + @field:SerializedName("id") + val id: Int? = null, + + @field:SerializedName("views") + val views: Int? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Views.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Views.kt new file mode 100644 index 0000000..12782ce --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/data/newsfeed/response/Views.kt @@ -0,0 +1,12 @@ +package com.terebenin.durov_return_the_wall.data.newsfeed.response + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Views( + + @field:SerializedName("count") + val count: Int? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/global/AccessToken.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/global/AccessToken.kt new file mode 100644 index 0000000..5b20058 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/global/AccessToken.kt @@ -0,0 +1,3 @@ +package com.terebenin.durov_return_the_wall.domain.global + +data class AccessToken(var token: String?, var expires_in: String?, var userId: String?) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/global/AuthError.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/global/AuthError.kt new file mode 100644 index 0000000..44a1d0d --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/global/AuthError.kt @@ -0,0 +1,3 @@ +package com.terebenin.durov_return_the_wall.domain.global + +data class AuthError(var error: String?, var description: String?) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/global/DateExt.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/global/DateExt.kt new file mode 100644 index 0000000..2687d9d --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/global/DateExt.kt @@ -0,0 +1,9 @@ +package com.terebenin.durov_return_the_wall.domain.global + +import org.threeten.bp.ZonedDateTime +import org.threeten.bp.format.DateTimeFormatter + +/** Format a time to show day, month, and time, e.g. "7 May 10:00" */ +fun ZonedDateTime.toDateTimeString(): String { + return DateTimeFormatter.ofPattern("d MMM h:mm").format(this) +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/NewsfeedInteractor.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/NewsfeedInteractor.kt new file mode 100644 index 0000000..728f7dc --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/NewsfeedInteractor.kt @@ -0,0 +1,11 @@ +package com.terebenin.durov_return_the_wall.domain.newsfeed + +import com.terebenin.durov_return_the_wall.data.datasource.network.VkApiFactory +import com.terebenin.durov_return_the_wall.data.newsfeed.NewsfeedRepositoryImpl + +class NewsfeedInteractor { + private val repository = NewsfeedRepositoryImpl(VkApiFactory.vkApi) + + suspend fun getNewsFeed() = repository.getNewsfeed() + +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/NewsfeedRepository.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/NewsfeedRepository.kt new file mode 100644 index 0000000..2e006ac --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/NewsfeedRepository.kt @@ -0,0 +1,7 @@ +package com.terebenin.durov_return_the_wall.domain.newsfeed + +import com.terebenin.durov_return_the_wall.domain.newsfeed.model.NewsfeedResponseDomainModel + +interface NewsfeedRepository { + suspend fun getNewsfeed(): NewsfeedResponseDomainModel? +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/GroupDomainModel.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/GroupDomainModel.kt new file mode 100644 index 0000000..ad85c2c --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/GroupDomainModel.kt @@ -0,0 +1,11 @@ +package com.terebenin.durov_return_the_wall.domain.newsfeed.model + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class GroupDomainModel( + val id: Int? = null, + val name: String? = null, + val photo100: String? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/NewsfeedResponseDomainModel.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/NewsfeedResponseDomainModel.kt new file mode 100644 index 0000000..3b02b7d --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/NewsfeedResponseDomainModel.kt @@ -0,0 +1,3 @@ +package com.terebenin.durov_return_the_wall.domain.newsfeed.model + +data class NewsfeedResponseDomainModel(val items: List?) \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/PostAuthorType.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/PostAuthorType.kt new file mode 100644 index 0000000..c41a8f4 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/PostAuthorType.kt @@ -0,0 +1,5 @@ +package com.terebenin.durov_return_the_wall.domain.newsfeed.model + +enum class PostAuthorType { + User, Group +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/PostItemDomainModel.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/PostItemDomainModel.kt new file mode 100644 index 0000000..6adee03 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/PostItemDomainModel.kt @@ -0,0 +1,32 @@ +package com.terebenin.durov_return_the_wall.domain.newsfeed.model + +import android.os.Parcelable +import com.terebenin.durov_return_the_wall.data.newsfeed.response.Attachments +import com.terebenin.durov_return_the_wall.data.newsfeed.response.Likes +import com.terebenin.durov_return_the_wall.data.newsfeed.response.Views +import kotlinx.android.parcel.Parcelize +import org.threeten.bp.ZonedDateTime + +@Parcelize +data class PostItemDomainModel( + + val sourceId: Int? = null, + + val date: ZonedDateTime, + + val postId: Int? = null, + + val text: String? = null, + + val views: Views? = null, + + val likes: Likes? = null, + + val profile: ProfileDomainModel? = null, + + val group: GroupDomainModel? = null, + + val postType: PostAuthorType? = null, + + val attachments: List? +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/ProfileDomainModel.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/ProfileDomainModel.kt new file mode 100644 index 0000000..a879ba9 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/domain/newsfeed/model/ProfileDomainModel.kt @@ -0,0 +1,12 @@ +package com.terebenin.durov_return_the_wall.domain.newsfeed.model + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class ProfileDomainModel( + val id: Int? = null, + val firstName: String? = null, + val lastName: String? = null, + val photo100: String? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/auth/AuthActivity.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/auth/AuthActivity.kt new file mode 100644 index 0000000..b084681 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/auth/AuthActivity.kt @@ -0,0 +1,99 @@ +package com.terebenin.durov_return_the_wall.presentation.auth + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.webkit.WebResourceRequest +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import com.terebenin.durov_return_the_wall.R +import com.terebenin.durov_return_the_wall.databinding.ActivityAuthBinding +import com.terebenin.durov_return_the_wall.presentation.global.MainActivity + +class AuthActivity : AppCompatActivity() { + + private lateinit var binding: ActivityAuthBinding + private val viewModel: AuthViewModel by viewModels() + private val ACCESS_TOKEN = "access_token" + private val ERROR = "error" + private val ERROR_DESCRIPTION = "error_description" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_auth) + binding.apply { + lifecycleOwner = this@AuthActivity + authViewModel = viewModel + } + observeViewModelChanges() + setupWebView() + binding.webView.loadUrl(viewModel.authUrl) + binding.executePendingBindings() + } + + private fun setupWebView() { + setWebViewClient() + setWebSettings(binding.webView.settings) + clearWebViewBeforeLoading(binding.webView) + } + + private fun setWebViewClient() { + binding.webView.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view?.loadUrl(request?.url.toString()) + binding.authViewModel?.currentUrl?.value = request?.url.toString() + } + return false + + } + } + } + + private fun observeViewModelChanges() { + + binding.authViewModel?.currentUrl?.observe(this, Observer { + if (it.contains(ACCESS_TOKEN)) { + binding.authViewModel?.setAccessToken(it) + } + if (it.contains(ERROR) || it.contains(ERROR_DESCRIPTION)) { + binding.authViewModel?.setError(it) + } + }) + + binding.authViewModel?.accessToken?.observe(this, Observer { + intent = Intent(this@AuthActivity, MainActivity::class.java) + startActivity(intent) + }) + + binding.authViewModel?.authError?.observe(this, Observer { + Toast.makeText(this, "${it.error}: ${it.description}", Toast.LENGTH_SHORT).show() + }) + } + + private fun clearWebViewBeforeLoading(webView: WebView) { + webView.apply { + clearHistory() + clearFormData() + clearCache(true) + } + } + + private fun setWebSettings(webSettings: WebSettings) { + webSettings.apply { + cacheMode = WebSettings.LOAD_NO_CACHE + domStorageEnabled = true + setSupportZoom(true) + javaScriptCanOpenWindowsAutomatically = true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/auth/AuthViewModel.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/auth/AuthViewModel.kt new file mode 100644 index 0000000..22bb18a --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/auth/AuthViewModel.kt @@ -0,0 +1,57 @@ +package com.terebenin.durov_return_the_wall.presentation.auth + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.terebenin.durov_return_the_wall.BuildConfig.* +import com.terebenin.durov_return_the_wall.domain.global.AccessToken +import com.terebenin.durov_return_the_wall.domain.global.AuthError +import com.terebenin.durov_return_the_wall.presentation.global.VkApplication + +class AuthViewModel : ViewModel() { + + val accessToken = MutableLiveData() + val currentUrl = MutableLiveData() + val authError = MutableLiveData() + + val authUrl = + AUTHORIZE_URI + + "?client_id=${APP_ID}" + + "&display=${DISPLAY_TYPE}" + + "&redirect_uri=${REDIRECT_URI}" + + "&scope=${SCOPE}" + + "&response_type=${RESPONSE_TYPE}" + + "&v=${API_VERSION}" + + fun setAccessToken(url: String) { + val token = url + .substringAfter("access_token=") + .substringBefore("&") + val expiresIn = url.substringAfter("&expires_in=") + .substringBefore("&") + val userId = url + .substringAfter("user_id=") + accessToken.value = + AccessToken( + token, + expiresIn, + userId + ) + saveAccessTokenToPrefs(accessToken.value!!) + } + + private fun saveAccessTokenToPrefs(it: AccessToken) { + VkApplication.prefs.accessToken = it + } + + fun setError(url: String) { + val error = url + .substringAfter("#error=") + .substringBefore("&") + val errorDescription = url.substringAfter("&error_description=").replace("+", " ") + authError.value = + AuthError( + error, + errorDescription + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/global/MainActivity.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/global/MainActivity.kt new file mode 100644 index 0000000..ea8e5bb --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/global/MainActivity.kt @@ -0,0 +1,30 @@ +package com.terebenin.durov_return_the_wall.presentation.global + +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.terebenin.durov_return_the_wall.R +import com.terebenin.durov_return_the_wall.presentation.global.VkApplication.Companion.prefs +import com.terebenin.durov_return_the_wall.presentation.newsfeed.NewsfeedFragment + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.main_activity) + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .replace( + R.id.container, + NewsfeedFragment.newInstance() + ) + .commitNow() + } + + //FIXME удалить код перед релизом + if (com.terebenin.durov_return_the_wall.BuildConfig.DEBUG) { + Toast.makeText(this, prefs.accessToken.token, Toast.LENGTH_SHORT).show() + } + } + +} diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/global/SingleLiveEvent.java b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/global/SingleLiveEvent.java new file mode 100644 index 0000000..18416ca --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/global/SingleLiveEvent.java @@ -0,0 +1,75 @@ +package com.terebenin.durov_return_the_wall.presentation.global; +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.util.Log; + +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + *

+ * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + *

+ * Note that only one observer is going to be notified of changes. + */ +public class SingleLiveEvent extends MutableLiveData { + + private static final String TAG = "SingleLiveEvent"; + + private final AtomicBoolean mPending = new AtomicBoolean(false); + + @MainThread + public void observe(LifecycleOwner owner, final Observer observer) { + + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); + } + + // Observe the internal MutableLiveData + super.observe(owner, new Observer() { + @Override + public void onChanged(@Nullable T t) { + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t); + } + } + }); + } + + @MainThread + public void setValue(@Nullable T t) { + mPending.set(true); + super.setValue(t); + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + public void call() { + setValue(null); + } +} diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/global/VkApplication.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/global/VkApplication.kt new file mode 100644 index 0000000..7f2a1e1 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/global/VkApplication.kt @@ -0,0 +1,42 @@ +package com.terebenin.durov_return_the_wall.presentation.global + +import android.app.Application +import android.content.Context +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.jakewharton.threetenabp.AndroidThreeTen +import com.terebenin.durov_return_the_wall.data.datasource.storage.Prefs + +class VkApplication : Application() { + + companion object { + private lateinit var context: Context + lateinit var gson: Gson + lateinit var prefs: Prefs + + fun setContext(con: Context) { + context = con + } + } + + override fun onCreate() { + super.onCreate() + setContext(this) + initGson() + initSharedPreferences() + initAndroidThreeTen() + } + + private fun initAndroidThreeTen() { + AndroidThreeTen.init(this) + } + + private fun initSharedPreferences() { + prefs = Prefs(context, gson) + } + + private fun initGson() { + gson = GsonBuilder() + .create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedAdapter.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedAdapter.kt new file mode 100644 index 0000000..78730ac --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedAdapter.kt @@ -0,0 +1,125 @@ +package com.terebenin.durov_return_the_wall.presentation.newsfeed + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.terebenin.durov_return_the_wall.R +import com.terebenin.durov_return_the_wall.databinding.ItemNewsfeedBinding +import com.terebenin.durov_return_the_wall.domain.global.toDateTimeString +import com.terebenin.durov_return_the_wall.domain.newsfeed.model.PostAuthorType +import com.terebenin.durov_return_the_wall.domain.newsfeed.model.PostItemDomainModel +import kotlinx.android.synthetic.main.item_newsfeed.view.* + + +class NewsfeedAdapter(private val viewModel: NewsfeedViewModel) : + ListAdapter( + DiffCallback() + ) { + private lateinit var context: Context + private val imageViewDoubleHeight: Int = 96 + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsfeedViewHolder { + return NewsfeedViewHolder( + ItemNewsfeedBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ), viewModel + ) + } + + override fun onBindViewHolder(holder: NewsfeedViewHolder, position: Int) { + holder.bind(getItem(position)) + holder.binding.item + context = holder.binding.textViewPostOwner.context + holder.itemView.text_view_post_owner.text = getPostOwnerName(holder.binding.item) + loadAvatar(holder) + holder.itemView.text_view_post_date.text = + holder.binding.item?.date!!.toDateTimeString() + + holder.itemView.text_view_images_count.text = + setTextAboutAttachedPhotos(holder.binding.item?.attachments!!.size) + } + + private fun setTextAboutAttachedPhotos(attachPhotosCount: Int): String { + return context.resources.getString( + R.string.text_how_many_photos_in_post, + context.resources.getQuantityString( + R.plurals.images, + attachPhotosCount, + attachPhotosCount + ) + ) + } + + private fun loadAvatar(holder: NewsfeedViewHolder) { + Glide.with( + holder.itemView.image_view_post_owner_avatar + ) + .load(getAvatarUrl(holder.binding.item)) + .centerCrop() + .transform(RoundedCorners(imageViewDoubleHeight)) + .into(holder.itemView.image_view_post_owner_avatar) + } + + private fun getAvatarUrl(item: PostItemDomainModel?): String { + return when (item?.postType) { + PostAuthorType.User -> { + item.profile?.photo100 ?: "" + } + PostAuthorType.Group -> { + item.group?.photo100 ?: "" + } + null -> "" + } + } + + private fun getPostOwnerName(item: PostItemDomainModel?): String { + return when (item?.postType) { + PostAuthorType.User -> { + context.getString( + R.string.text_user_name, + item.profile?.firstName, + item.profile?.lastName + ) + } + PostAuthorType.Group -> { + item.group?.name ?: "" + } + null -> "" + } + } + + class NewsfeedViewHolder( + val binding: ItemNewsfeedBinding, + private val viewModel: NewsfeedViewModel + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(model: PostItemDomainModel) { + binding.eventHandler = viewModel + binding.item = model + binding.executePendingBindings() + } + + } + + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: PostItemDomainModel, + newItem: PostItemDomainModel + ): Boolean { + return oldItem.postId == newItem.postId + } + + override fun areContentsTheSame( + oldItem: PostItemDomainModel, + newItem: PostItemDomainModel + ): Boolean { + return oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedEventHandler.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedEventHandler.kt new file mode 100644 index 0000000..c62080b --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedEventHandler.kt @@ -0,0 +1,7 @@ +package com.terebenin.durov_return_the_wall.presentation.newsfeed + +import com.terebenin.durov_return_the_wall.domain.newsfeed.model.PostItemDomainModel + +interface NewsfeedEventHandler { + fun onClickNewsfeedItem(item: PostItemDomainModel) +} \ No newline at end of file diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedFragment.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedFragment.kt new file mode 100644 index 0000000..20fb658 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedFragment.kt @@ -0,0 +1,76 @@ +package com.terebenin.durov_return_the_wall.presentation.newsfeed + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import com.terebenin.durov_return_the_wall.R +import com.terebenin.durov_return_the_wall.databinding.MainFragmentBinding + +class NewsfeedFragment : Fragment() { + + companion object { + fun newInstance() = + NewsfeedFragment() + } + + private lateinit var binding: MainFragmentBinding + private lateinit var newsfeedAdapter: NewsfeedAdapter + private val viewModel: NewsfeedViewModel by viewModels() + + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DataBindingUtil.inflate( + inflater, + R.layout.main_fragment, + container, + false + ) + return binding.root + } + + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + binding.apply { + lifecycleOwner = this@NewsfeedFragment + vm = viewModel + executePendingBindings() + } + initAdapter(viewModel) + observeViewModelChanges(binding) + } + + private fun observeViewModelChanges(binding: MainFragmentBinding) { + binding.vm?.newsfeedResponse?.observe(viewLifecycleOwner, Observer { + newsfeedAdapter.submitList(it.items) + }) + + binding.vm?.itemClickEvent?.observe(viewLifecycleOwner, Observer { + //TODO + Toast.makeText(context, "клик на пост с ID $it", Toast.LENGTH_SHORT).show() + }) + } + + private fun initAdapter(viewModel: NewsfeedViewModel) { + newsfeedAdapter = + NewsfeedAdapter( + viewModel + ) + binding.recyclerViewNewsfeed.apply { + layoutManager = LinearLayoutManager(context) + adapter = newsfeedAdapter + } + } + +} diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedViewModel.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedViewModel.kt new file mode 100644 index 0000000..2ebfb76 --- /dev/null +++ b/app/src/main/java/com/terebenin/durov_return_the_wall/presentation/newsfeed/NewsfeedViewModel.kt @@ -0,0 +1,53 @@ +package com.terebenin.durov_return_the_wall.presentation.newsfeed + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.terebenin.durov_return_the_wall.domain.newsfeed.NewsfeedInteractor +import com.terebenin.durov_return_the_wall.domain.newsfeed.model.NewsfeedResponseDomainModel +import com.terebenin.durov_return_the_wall.domain.newsfeed.model.PostItemDomainModel +import com.terebenin.durov_return_the_wall.presentation.global.SingleLiveEvent +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import retrofit2.HttpException + +class NewsfeedViewModel : ViewModel(), + NewsfeedEventHandler { + private val interactor = NewsfeedInteractor() + val itemClickEvent = + SingleLiveEvent() + val eventHttpException = + SingleLiveEvent() + + val newsfeedResponse = MutableLiveData() + val isLoading = MutableLiveData() + private var job: Job? = null + + init { + getNewsfeed() + } + + + fun getNewsfeed() { + job?.cancel() + job = viewModelScope.launch { + try { + isLoading.value = true + val result = interactor.getNewsFeed() + result?.let { + newsfeedResponse.value = it + isLoading.value = false + } + } catch (e: Exception) { + if (e is HttpException) { + eventHttpException.value = e + } else e.printStackTrace() + } + } + } + + override fun onClickNewsfeedItem(item: PostItemDomainModel) { + itemClickEvent.value = item.postId + } + +} diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/ui/main/MainFragment.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/ui/main/MainFragment.kt deleted file mode 100644 index 159ebb0..0000000 --- a/app/src/main/java/com/terebenin/durov_return_the_wall/ui/main/MainFragment.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.terebenin.durov_return_the_wall.ui.main - -import androidx.lifecycle.ViewModelProviders -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.terebenin.durov_return_the_wall.R - -class MainFragment : Fragment() { - - companion object { - fun newInstance() = MainFragment() - } - - private lateinit var viewModel: MainViewModel - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return inflater.inflate(R.layout.main_fragment, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) - // TODO: Use the ViewModel - } - -} diff --git a/app/src/main/java/com/terebenin/durov_return_the_wall/ui/main/MainViewModel.kt b/app/src/main/java/com/terebenin/durov_return_the_wall/ui/main/MainViewModel.kt deleted file mode 100644 index 23dafe7..0000000 --- a/app/src/main/java/com/terebenin/durov_return_the_wall/ui/main/MainViewModel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.terebenin.durov_return_the_wall.ui.main - -import androidx.lifecycle.ViewModel - -class MainViewModel : ViewModel() { - // TODO: Implement the ViewModel -} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 1f6bb29..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 0d025f9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_outline_favorite_border_24.xml b/app/src/main/res/drawable/ic_outline_favorite_border_24.xml new file mode 100644 index 0000000..ba9a646 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_favorite_border_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_remove_red_eye_16.xml b/app/src/main/res/drawable/ic_outline_remove_red_eye_16.xml new file mode 100644 index 0000000..0278b78 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_remove_red_eye_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_auth.xml b/app/src/main/res/layout/activity_auth.xml new file mode 100644 index 0000000..1d95dae --- /dev/null +++ b/app/src/main/res/layout/activity_auth.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_newsfeed.xml b/app/src/main/res/layout/item_newsfeed.xml new file mode 100644 index 0000000..a50789e --- /dev/null +++ b/app/src/main/res/layout/item_newsfeed.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index 1de2219..5f47dfe 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -4,4 +4,4 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity" /> + tools:context=".presentation.global.MainActivity" /> diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml index d27636e..ec35688 100644 --- a/app/src/main/res/layout/main_fragment.xml +++ b/app/src/main/res/layout/main_fragment.xml @@ -1,20 +1,30 @@ - + - + - + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index eca70cf..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index eca70cf..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 898f3ed..9b53e8e 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index dffca36..92bea99 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index 64ba76f..a51e27f 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index dae5e08..eba7b06 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index e5ed465..5f3207c 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 14ed0af..3ea2460 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index b0907ca..bf07cbf 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index d8ae031..5629090 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 2c18de9..844e5ff 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index beed3cd..48dffe4 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a63121..460278c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,15 @@ - Durov_return_the_wall + Дуров, верни стену! + Новости + %1$s %2$s + В посте %1$s. Нажмите, чтобы посмотреть + + + %d изображений + %d изображение + %d изображения + %d изображения + %d изображений + %d изображений + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5885930..460f967 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ -