From: Piotr Ostrowski Date: Mon, 24 Dec 2018 12:37:02 +0000 (+0100) Subject: Added Android code X-Git-Url: https://git.mdrn.pl/wl-app.git/commitdiff_plain/HEAD?ds=inline Added Android code --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3129f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +/Android/local.properties +/Android/.idea +.DS_Store +/Android/build +/Android/folioreader/build +/Android/webViewMarker/build +/Android*.iml +/Android/folioreader/*.iml +/Android/webViewMarker/*.iml +/Android/captures +.externalNativeBuild +/Androidapp/release/* +/Android/r2-streamer/r2-fetcher/build +/Android/r2-streamer/r2-parser/build +/Android/r2-streamer/r2-server/build diff --git a/Android/app/build.gradle b/Android/app/build.gradle new file mode 100644 index 0000000..c17a047 --- /dev/null +++ b/Android/app/build.gradle @@ -0,0 +1,108 @@ +apply plugin: 'com.android.application' +apply plugin: 'io.objectbox' +apply plugin: 'io.fabric' + +android { + compileSdkVersion 27 + defaultConfig { + applicationId "com.moiseum.wolnelektury" + minSdkVersion 19 + targetSdkVersion 27 + versionCode 5 + versionName "2.0.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + multiDexEnabled true + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + buildToolsVersion '27.0.3' + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + dexOptions { + jumboMode true + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support.constraint:constraint-layout:1.1.1' + implementation 'com.android.support:recyclerview-v7:27.1.1' + implementation 'com.android.support:design:27.1.1' + implementation 'com.android.support:cardview-v7:27.1.1' + implementation 'com.android.support:customtabs:27.1.1' + implementation 'com.android.support:multidex:1.0.3' + + // Butterknife + implementation 'com.jakewharton:butterknife:8.8.1' + annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' + + // Retrofit + implementation 'com.squareup.retrofit2:retrofit:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.3.0' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' + implementation 'com.squareup.okhttp3:logging-interceptor:3.7.0' + + // RxJava + implementation 'io.reactivex.rxjava2:rxjava:2.1.5' + implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' + + // Eventbus + implementation 'org.greenrobot:eventbus:3.1.1' + + // parceler + implementation 'org.parceler:parceler-api:1.1.6' + annotationProcessor 'org.parceler:parceler:1.1.6' + + // Glide + implementation 'com.github.bumptech.glide:glide:3.7.0' + + // FlowLayout + implementation 'com.nex3z:flow-layout:1.1.0' + + // ShimmerEffect + implementation 'com.facebook.shimmer:shimmer:0.1.0@aar' + + // htmltextview + implementation 'org.sufficientlysecure:html-textview:3.5' + + // Piwik + implementation 'org.piwik.sdk:piwik-sdk:3.0.2' + + // SecuredSharedPreferences + implementation 'de.adorsys.android:securestoragelibrary:1.0.2' + + // HtmlTextView + implementation 'org.sufficientlysecure:html-textview:3.6' + + // ViewPagerIndicator + implementation 'me.relex:circleindicator:1.2.2@aar' + + // ZoomView + implementation 'it.sephiroth.android.library.imagezoom:imagezoom:2.3.0' + + // Fabric Crashlytics + implementation('com.crashlytics.sdk.android:crashlytics:2.9.5@aar') { + transitive = true + } + + // FCM + implementation 'com.google.firebase:firebase-core:16.0.3' + implementation 'com.google.firebase:firebase-messaging:17.3.0' + + // Tests + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.1' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + + // Folio Reader + implementation project(path: ':folioreader') +} + +apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/Android/app/google-services.json b/Android/app/google-services.json new file mode 100644 index 0000000..e69de29 diff --git a/Android/app/objectbox-models/default.json b/Android/app/objectbox-models/default.json new file mode 100644 index 0000000..64de239 --- /dev/null +++ b/Android/app/objectbox-models/default.json @@ -0,0 +1,138 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:1266212408658120333", + "lastPropertyId": "27:6081938606404169841", + "name": "BookModel", + "properties": [ + { + "id": "1:4875450393257016076", + "name": "localId" + }, + { + "id": "2:6442526149769234951", + "name": "kind" + }, + { + "id": "3:6139716336211719332", + "name": "author" + }, + { + "id": "4:9122866387478688597", + "name": "url" + }, + { + "id": "5:2911729206207838262", + "name": "title" + }, + { + "id": "6:6733284022068068913", + "name": "cover" + }, + { + "id": "7:2855699566794978818", + "name": "epoch" + }, + { + "id": "8:3155585116505368178", + "name": "href" + }, + { + "id": "9:5758122160014138064", + "name": "genre" + }, + { + "id": "10:4565534831058503932", + "name": "slug" + }, + { + "id": "11:2010568180259287879", + "name": "coverThumb" + }, + { + "id": "14:1346463204325727422", + "name": "key" + }, + { + "id": "15:7680817761521283170", + "name": "hasAudio" + }, + { + "id": "18:147986086857220058", + "name": "currentChapter" + }, + { + "id": "19:8713447497975671246", + "name": "totalChapters" + }, + { + "id": "20:8513709726333847808", + "name": "ebookFileUrl" + }, + { + "id": "21:4938160215891531703", + "name": "currentAudioChapter" + }, + { + "id": "22:4218282539034900953", + "name": "totalAudioChapters" + }, + { + "id": "23:4885260030438873466", + "name": "audioFileUrls" + }, + { + "id": "24:6566362314372860102", + "name": "coverColor" + }, + { + "id": "25:782192741435330189", + "name": "liked" + }, + { + "id": "26:4213249825017494295", + "name": "sortedKey" + }, + { + "id": "27:6081938606404169841", + "name": "ebookName" + } + ], + "relations": [] + } + ], + "lastEntityId": "2:1504498481041860157", + "lastIndexId": "1:6468504721746553676", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 4, + "modelVersionParserMinimum": 4, + "retiredEntityUids": [ + 1504498481041860157 + ], + "retiredIndexUids": [ + 6468504721746553676 + ], + "retiredPropertyUids": [ + 6803086463971496152, + 3080154473130007804, + 1621280139326722999, + 2103818903142647350, + 6713758539027593090, + 2491180996489637688, + 4042232117924105743, + 5735503626971019687, + 8275039331603704005, + 2881981269631671029, + 8276445823965894226, + 3807782664163355511, + 8710097352585536243, + 8415046264594345414, + 1929729267744483000 + ], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/Android/app/objectbox-models/default.json.bak b/Android/app/objectbox-models/default.json.bak new file mode 100644 index 0000000..15b567d --- /dev/null +++ b/Android/app/objectbox-models/default.json.bak @@ -0,0 +1,134 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:1266212408658120333", + "lastPropertyId": "26:4213249825017494295", + "name": "BookModel", + "properties": [ + { + "id": "1:4875450393257016076", + "name": "localId" + }, + { + "id": "2:6442526149769234951", + "name": "kind" + }, + { + "id": "3:6139716336211719332", + "name": "author" + }, + { + "id": "4:9122866387478688597", + "name": "url" + }, + { + "id": "5:2911729206207838262", + "name": "title" + }, + { + "id": "6:6733284022068068913", + "name": "cover" + }, + { + "id": "7:2855699566794978818", + "name": "epoch" + }, + { + "id": "8:3155585116505368178", + "name": "href" + }, + { + "id": "9:5758122160014138064", + "name": "genre" + }, + { + "id": "10:4565534831058503932", + "name": "slug" + }, + { + "id": "11:2010568180259287879", + "name": "coverThumb" + }, + { + "id": "14:1346463204325727422", + "name": "key" + }, + { + "id": "15:7680817761521283170", + "name": "hasAudio" + }, + { + "id": "18:147986086857220058", + "name": "currentChapter" + }, + { + "id": "19:8713447497975671246", + "name": "totalChapters" + }, + { + "id": "20:8513709726333847808", + "name": "ebookFileUrl" + }, + { + "id": "21:4938160215891531703", + "name": "currentAudioChapter" + }, + { + "id": "22:4218282539034900953", + "name": "totalAudioChapters" + }, + { + "id": "23:4885260030438873466", + "name": "audioFileUrls" + }, + { + "id": "24:6566362314372860102", + "name": "coverColor" + }, + { + "id": "25:782192741435330189", + "name": "liked" + }, + { + "id": "26:4213249825017494295", + "name": "sortedKey" + } + ], + "relations": [] + } + ], + "lastEntityId": "2:1504498481041860157", + "lastIndexId": "1:6468504721746553676", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 4, + "modelVersionParserMinimum": 4, + "retiredEntityUids": [ + 1504498481041860157 + ], + "retiredIndexUids": [ + 6468504721746553676 + ], + "retiredPropertyUids": [ + 6803086463971496152, + 3080154473130007804, + 1621280139326722999, + 2103818903142647350, + 6713758539027593090, + 2491180996489637688, + 4042232117924105743, + 5735503626971019687, + 8275039331603704005, + 2881981269631671029, + 8276445823965894226, + 3807782664163355511, + 8710097352585536243, + 8415046264594345414, + 1929729267744483000 + ], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/Android/app/proguard-rules.pro b/Android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/Android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/Android/app/src/androidTest/java/com/moiseum/wolnelektury/ExampleInstrumentedTest.java b/Android/app/src/androidTest/java/com/moiseum/wolnelektury/ExampleInstrumentedTest.java new file mode 100644 index 0000000..35def5e --- /dev/null +++ b/Android/app/src/androidTest/java/com/moiseum/wolnelektury/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.moiseum.wolnelektury; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.moiseum.wolnelektury", appContext.getPackageName()); + } +} diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a9e83c0 --- /dev/null +++ b/Android/app/src/main/AndroidManifest.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractActivity.java new file mode 100644 index 0000000..a4c3695 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractActivity.java @@ -0,0 +1,161 @@ +package com.moiseum.wolnelektury.base; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; +import android.widget.Toast; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; +import com.moiseum.wolnelektury.view.login.LoginActivity; + +import java.util.List; + +import butterknife.ButterKnife; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +public abstract class AbstractActivity extends AppCompatActivity { + + private static final String CHROME_PACKAGE_ID = "com.android.chrome"; + + private CompositeDisposable disposables = new CompositeDisposable(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // ActionBar actionBar = getSupportActionBar(); + // if (actionBar != null) { + // actionBar.setElevation(0); + // } + + setContentView(getLayoutResourceId()); + ButterKnife.bind(this); + prepareView(savedInstanceState); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onHomeClicked(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + disposables.dispose(); + } + + protected void addDisposable(Disposable disposable) { + this.disposables.add(disposable); + } + + protected void onHomeClicked() { + finish(); + } + + protected void setBackButtonEnable(boolean enable) { + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(enable); + } + } + + protected void hideToolbar() { + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.hide(); + } + } + + protected void setupToolbar(Toolbar toolbar) { + if (toolbar != null) { + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + } + + protected void showPayPalForm() { + SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + if (preferences.isUserLoggedIn()) { + showBrowserView(Uri.parse(RestClient.WEB_PAYPAL_FORM_URL)); + } else { + startActivity(new LoginActivity.LoginIntent(this)); + } + } + + protected void showBrowserView(Uri uri) { + if (checkForPackageExistance(CHROME_PACKAGE_ID)) { + CustomTabsServiceConnection connection = new CustomTabsServiceConnection() { + @Override + public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) { + CustomTabsIntent intent = new CustomTabsIntent.Builder() + .setToolbarColor(ContextCompat.getColor(AbstractActivity.this, R.color.colorAccent)) + .build(); + + client.warmup(0L); + intent.launchUrl(AbstractActivity.this, uri); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + + } + }; + CustomTabsClient.bindCustomTabsService(this, CHROME_PACKAGE_ID, connection); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + if (intent.resolveActivity(getPackageManager()) != null) { + startActivity(intent); + } else { + Toast.makeText(this, R.string.install_chrome, Toast.LENGTH_LONG).show(); + } + } + } + + private boolean checkForPackageExistance(String targetPackage) { + List packages; + PackageManager pm; + + pm = getPackageManager(); + packages = pm.getInstalledApplications(0); + for (ApplicationInfo packageInfo : packages) { + if (packageInfo.packageName.equals(targetPackage)) { + return true; + } + } + return false; + } + + /** + * Providing layout resource ID for inflating. + * + * @return layout resource ID. + */ + public abstract int getLayoutResourceId(); + + /** + * Method called from @link{{@link AbstractActivity}#onCreate}. This will be the place to setup view stuff. + * + * @param savedInstanceState Bundle with current instance state. + */ + public abstract void prepareView(Bundle savedInstanceState); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractFragment.java new file mode 100644 index 0000000..5a0783b --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractFragment.java @@ -0,0 +1,157 @@ +package com.moiseum.wolnelektury.base; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; +import com.moiseum.wolnelektury.utils.TrackerUtils; +import com.moiseum.wolnelektury.view.login.LoginActivity; + +import java.util.List; + +import butterknife.ButterKnife; +import butterknife.Unbinder; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +/** + * Base fragment with view binding. + */ +public abstract class AbstractFragment extends Fragment { + + private static final String CHROME_PACKAGE_ID = "com.android.chrome"; + + private Unbinder unbinder; + private CompositeDisposable disposables = new CompositeDisposable(); + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(getLayoutResourceId(), container, false); + unbinder = ButterKnife.bind(this, view); + prepareView(view, savedInstanceState); + trackScreen(); + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + disposables.dispose(); + } + + protected void addDisposable(Disposable disposable) { + this.disposables.add(disposable); + } + + private void trackScreen() { + String path = getClass().getPackage().getName(); + String name = getNameForTracker(); + TrackerUtils.trackScreen(path, name); + } + + protected String getNameForTracker() { + return getClass().getSimpleName().replaceAll("Fragment", ""); + } + + protected void setupToolbar(Toolbar toolbar) { + AbstractActivity activity = (AbstractActivity) getActivity(); + if (activity != null) { + activity.setupToolbar(toolbar); + } + } + + protected void showPayPalForm() { + SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + if (preferences.isUserLoggedIn()) { + showBrowserView(Uri.parse(RestClient.WEB_PAYPAL_FORM_URL)); + } else { + startActivity(new LoginActivity.LoginIntent(getContext())); + } + } + + protected void showBrowserView(Uri uri) { + if (getActivity() != null) { + if (checkForPackageExistance(CHROME_PACKAGE_ID)) { + CustomTabsServiceConnection connection = new CustomTabsServiceConnection() { + @Override + public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) { + CustomTabsIntent intent = new CustomTabsIntent.Builder() + .setToolbarColor(ContextCompat.getColor(getActivity(), R.color.colorAccent)) + .build(); + + client.warmup(0L); + intent.launchUrl(getActivity(), uri); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + + } + }; + CustomTabsClient.bindCustomTabsService(getActivity(), CHROME_PACKAGE_ID, connection); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + if (intent.resolveActivity(getActivity().getPackageManager()) != null) { + startActivity(intent); + } else { + Toast.makeText(getActivity(), R.string.install_chrome, Toast.LENGTH_LONG).show(); + } + } + } + } + + protected void showShareActivity(String shareUrl) { + Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND); + sharingIntent.setType("text/plain"); + sharingIntent.putExtra(Intent.EXTRA_TEXT, shareUrl); + startActivity(Intent.createChooser(sharingIntent, getString(R.string.share))); + } + + private boolean checkForPackageExistance(String targetPackage) { + List packages; + PackageManager pm; + + pm = getActivity().getPackageManager(); + packages = pm.getInstalledApplications(0); + for (ApplicationInfo packageInfo : packages) { + if (packageInfo.packageName.equals(targetPackage)) { + return true; + } + } + return false; + } + + /** + * Providing layout resource ID for inflating. + * + * @return layout resource ID. + */ + public abstract int getLayoutResourceId(); + + /** + * Method called from @link{BindingFragment#onCreateView}. This will be the place to setup view stuff. + * + * @param view inflated View. + * @param savedInstanceState Bundle with current instance state. + */ + public abstract void prepareView(View view, Bundle savedInstanceState); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractIntent.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractIntent.java new file mode 100644 index 0000000..3127710 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractIntent.java @@ -0,0 +1,16 @@ +package com.moiseum.wolnelektury.base; + +import android.content.Context; +import android.content.Intent; + +/** + * Base abstract intent for activities. + */ + +public abstract class AbstractIntent extends Intent { + + public AbstractIntent(final Context packageContext, final Class cls) { + super(packageContext, cls); + } +} + diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/DataObserver.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/DataObserver.java new file mode 100644 index 0000000..607a340 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/DataObserver.java @@ -0,0 +1,12 @@ +package com.moiseum.wolnelektury.base; + +/** + * @author golonkos. + */ +public interface DataObserver { + void onLoadStarted(); + + void onLoadSuccess(T data); + + void onLoadFailed(Exception e); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/DataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/DataProvider.java new file mode 100644 index 0000000..a0663ba --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/DataProvider.java @@ -0,0 +1,75 @@ +package com.moiseum.wolnelektury.base; + +import android.util.Log; + +import com.moiseum.wolnelektury.connection.RestClientCallback; + +import javax.annotation.Nullable; + +import retrofit2.Call; + +/** + * @author golonkos. + */ + +public abstract class DataProvider extends RestClientCallback { + + private final static String TAG = DataProvider.class.getSimpleName(); + + protected DataObserver dataObserver; + protected String lastKeySlug = null; + private Call call; + + public DataProvider() { + } + + public void setDataObserver(DataObserver dataObserver) { + this.dataObserver = dataObserver; + } + + @Override + public void onSuccess(T data) { + if (dataObserver != null) { + dataObserver.onLoadSuccess(data); + } + } + + @Override + public void onFailure(Exception e) { + Log.e(TAG, "Failed to load data", e); + if (dataObserver != null) { + dataObserver.onLoadFailed(e); + } + } + + @Override + public void onCancel() { + //nop + } + + /** + * Invoked in order to load data. + * @param lastKey Last book slug for pagination. Can be null if there is no pagination. + */ + public void load(@Nullable String lastKey) { + cancel(); + lastKeySlug = lastKey; + call = WLApplication.getInstance().getRestClient().call(this, getServiceClass()); + if (dataObserver != null) { + dataObserver.onLoadStarted(); + } + } + + public void cancel() { + if (call != null) { + call.cancel(); + call = null; + } + } + + public void release() { + dataObserver = null; + } + + protected abstract Class getServiceClass(); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/WLApplication.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/WLApplication.java new file mode 100644 index 0000000..c72915b --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/WLApplication.java @@ -0,0 +1,65 @@ +package com.moiseum.wolnelektury.base; + +import android.support.multidex.MultiDexApplication; + +import com.crashlytics.android.Crashlytics; +import com.crashlytics.android.core.CrashlyticsCore; +import com.moiseum.wolnelektury.BuildConfig; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.storage.BookStorage; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; +import com.moiseum.wolnelektury.utils.TrackerUtils; + +import org.piwik.sdk.Tracker; + +import io.fabric.sdk.android.Fabric; + +public class WLApplication extends MultiDexApplication { + + private static WLApplication instance; + private RestClient restClient; + private BookStorage bookStorage; + private Tracker tracker; + private SharedPreferencesUtils preferences; + + @Override + public void onCreate() { + super.onCreate(); + instance = this; + bookStorage = new BookStorage(this); + + Crashlytics crashlytics = new Crashlytics.Builder() + .core(new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()) + .build(); + Fabric.with(this, new Crashlytics()); + } + + public static WLApplication getInstance() { + return instance; + } + + public RestClient getRestClient() { + if (restClient == null) { + restClient = new RestClient(getApplicationContext()); + } + return restClient; + } + + public BookStorage getBookStorage() { + return bookStorage; + } + + public synchronized Tracker getTracker() { + if (tracker == null) { + tracker = TrackerUtils.create(this); + } + return tracker; + } + + public SharedPreferencesUtils getPreferences() { + if (preferences == null) { + preferences = new SharedPreferencesUtils(this); + } + return preferences; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/FragmentLifecyclePresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/FragmentLifecyclePresenter.java new file mode 100644 index 0000000..6b2d897 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/FragmentLifecyclePresenter.java @@ -0,0 +1,15 @@ +package com.moiseum.wolnelektury.base.mvp; + +import android.os.Bundle; + +/** + * Created by Piotr Ostrowski on 13.06.2018. + */ +public abstract class FragmentLifecyclePresenter extends LifecyclePresenter { + + public void onViewCreated(Bundle savedInstanceState) { + } + + public void onDestroyView() { + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/FragmentPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/FragmentPresenter.java new file mode 100644 index 0000000..ac4587a --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/FragmentPresenter.java @@ -0,0 +1,21 @@ +package com.moiseum.wolnelektury.base.mvp; + +public class FragmentPresenter extends FragmentLifecyclePresenter { + + private V view; + + public FragmentPresenter(V view) { + this.view = view; + } + + @Override + public void onDestroy() { + super.onDestroy(); + this.view = null; + } + + protected V getView() { + return view; + } + +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/LifecyclePresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/LifecyclePresenter.java new file mode 100644 index 0000000..e7aa0b5 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/LifecyclePresenter.java @@ -0,0 +1,39 @@ +package com.moiseum.wolnelektury.base.mvp; + +import android.os.Bundle; +import android.support.annotation.CallSuper; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +public abstract class LifecyclePresenter { + + private CompositeDisposable disposables = new CompositeDisposable(); + + public void onCreate(Bundle savedInstanceState) { + } + + public void onStart() { + } + + public void onStop() { + } + + public void onResume() { + } + + public void onPause() { + } + + @CallSuper + public void onDestroy() { + disposables.dispose(); + } + + public void onSaveInstanceState(Bundle outState) { + } + + protected void addDisposable(Disposable disposable) { + disposables.add(disposable); + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/LoadingView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/LoadingView.java new file mode 100644 index 0000000..98c4095 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/LoadingView.java @@ -0,0 +1,10 @@ +package com.moiseum.wolnelektury.base.mvp; + +public interface LoadingView { + + void setData(T data); + + void setProgressVisible(boolean visible); + + void showError(Exception e); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PaginableLoadingView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PaginableLoadingView.java new file mode 100644 index 0000000..c7fbb42 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PaginableLoadingView.java @@ -0,0 +1,16 @@ +package com.moiseum.wolnelektury.base.mvp; + +/** + * Created by Piotr Ostrowski on 28.11.2017. + */ + +public interface PaginableLoadingView { + + void setData(T data, boolean reload); + + void setInitialProgressVisible(boolean visible); + + void setLoadMoreProgressVisible(boolean visible); + + void showError(Exception e, boolean loadMore); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/Presenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/Presenter.java new file mode 100644 index 0000000..acad92f --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/Presenter.java @@ -0,0 +1,24 @@ +package com.moiseum.wolnelektury.base.mvp; + +/** + * Created by Piotr Ostrowski on 13.06.2018. + */ +public class Presenter extends LifecyclePresenter { + + private V view; + + public Presenter(V view) { + this.view = view; + } + + @Override + public void onDestroy() { + super.onDestroy(); + this.view = null; + } + + protected V getView() { + return view; + } + +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterActivity.java new file mode 100644 index 0000000..d235018 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterActivity.java @@ -0,0 +1,62 @@ +package com.moiseum.wolnelektury.base.mvp; + +import android.os.Bundle; + +import com.moiseum.wolnelektury.base.AbstractActivity; + +/** + * Created by Piotr Ostrowski on 13.06.2018. + */ +public abstract class PresenterActivity

extends AbstractActivity { + + private P presenter; + + protected abstract P createPresenter(); + + protected P getPresenter() { + return presenter; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + presenter = createPresenter(); + presenter.onCreate(savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + presenter.onStart(); + } + + @Override + public void onStop() { + super.onStop(); + presenter.onStop(); + } + + @Override + public void onResume() { + super.onResume(); + presenter.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + presenter.onPause(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + presenter.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + presenter.onSaveInstanceState(outState); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterFragment.java new file mode 100644 index 0000000..b7a0dce --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterFragment.java @@ -0,0 +1,80 @@ +package com.moiseum.wolnelektury.base.mvp; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.view.View; + +import com.moiseum.wolnelektury.base.AbstractFragment; + + +/** + * Fragment that creates {@link LifecyclePresenter} and in its lifecycle methods calls corresponding methods of + * presenter. + * + * @param

type of presenter for this fragment. + */ +public abstract class PresenterFragment

extends AbstractFragment { + + private P presenter; + + protected abstract P createPresenter(); + + protected P getPresenter() { + return presenter; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + presenter = createPresenter(); + presenter.onCreate(savedInstanceState); + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + presenter.onViewCreated(savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + presenter.onStart(); + } + + @Override + public void onStop() { + super.onStop(); + presenter.onStop(); + } + + @Override + public void onResume() { + super.onResume(); + presenter.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + presenter.onPause(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + presenter.onDestroyView(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + presenter.onDestroy(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + presenter.onSaveInstanceState(outState); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/CheckableRelativeLayout.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/CheckableRelativeLayout.java new file mode 100644 index 0000000..5a19829 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/CheckableRelativeLayout.java @@ -0,0 +1,40 @@ +package com.moiseum.wolnelektury.components; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Checkable; +import android.widget.RelativeLayout; + +/** + * Relative layout for handling checking/selecting action. + */ +public class CheckableRelativeLayout extends RelativeLayout implements Checkable { + + public CheckableRelativeLayout(Context context) { + super(context); + } + + public CheckableRelativeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void setChecked(boolean checked) { + super.setSelected(checked); + } + + @Override + public boolean isChecked() { + return super.isSelected(); + } + + @Override + public void toggle() { + super.setSelected(!isSelected()); + } +} + diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/ProgressRecyclerView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/ProgressRecyclerView.java new file mode 100644 index 0000000..cccb42a --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/ProgressRecyclerView.java @@ -0,0 +1,141 @@ +package com.moiseum.wolnelektury.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +/** + * @author golonkos + */ + +public class ProgressRecyclerView extends FrameLayout { + + public interface ProgressRecycleViewRetryListener { + void onRetryClicked(); + } + + @BindView(R.id.rvList) + RecyclerView rvList; + @BindView(R.id.tvEmpty) + TextView tvEmpty; + @BindView(R.id.pbLoading) + ProgressBar pbLoading; + @BindView(R.id.ibRetry) + ImageButton ibRetry; + + private RecyclerAdapter adapter; + private ProgressRecycleViewRetryListener listener; + + public ProgressRecyclerView(@NonNull Context context) { + this(context, null); + } + + public ProgressRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ProgressRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs); + } + + private void init(AttributeSet attrs) { + View view = LayoutInflater.from(getContext()).inflate(R.layout.progress_recyclerview, this, true); + ButterKnife.bind(this, view); + + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ProgressRecyclerView); + try { + if (a.hasValue(R.styleable.ProgressRecyclerView_emptyText)) { + tvEmpty.setText(a.getString(R.styleable.ProgressRecyclerView_emptyText)); + } + } finally { + a.recycle(); + } + } + + public void setAdapter(RecyclerAdapter adapter) { + this.adapter = adapter; + rvList.setAdapter(adapter); + } + + public void setItems(List items) { + if (adapter == null) { + throw new UnsupportedOperationException("Adapter not set"); + } + adapter.setItems(items); + tvEmpty.setVisibility(items.isEmpty() ? VISIBLE : GONE); + } + + public void addItems(List items) { + if (adapter == null) { + throw new UnsupportedOperationException("Adapter not set"); + } + if (items.size() > 0) { + adapter.addItems(items); + } + } + + public void setProgressVisible(boolean visible) { + pbLoading.setVisibility(visible ? VISIBLE : GONE); + if (visible) { + tvEmpty.setVisibility(GONE); + } + } + + public void showRetryButton(ProgressRecycleViewRetryListener listener) { + this.listener = listener; + tvEmpty.setVisibility(GONE); + ibRetry.setVisibility(VISIBLE); + } + + public void setEmptyText(@StringRes int stringResId) { + tvEmpty.setText(stringResId); + } + + public void setLayoutManager(RecyclerView.LayoutManager layoutManager) { + rvList.setLayoutManager(layoutManager); + } + + public void setHasFixedSize(boolean fixedSize) { + rvList.setHasFixedSize(fixedSize); + } + + public void addOnScrollListener(RecyclerView.OnScrollListener listener) { + rvList.addOnScrollListener(listener); + } + + public void removeOnScrollListener(RecyclerView.OnScrollListener listener) { + rvList.removeOnScrollListener(listener); + } + + @OnClick(R.id.ibRetry) + public void retryButtonClick() { + if (listener != null) { + listener.onRetryClicked(); + } + ibRetry.setVisibility(GONE); + } + + public void updateEmptyViewVisibility() { + tvEmpty.setVisibility(ibRetry.getVisibility() == GONE && adapter.getItems().isEmpty() ? VISIBLE : GONE); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/ZoomableViewPager.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/ZoomableViewPager.java new file mode 100644 index 0000000..1cfdb52 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/ZoomableViewPager.java @@ -0,0 +1,113 @@ +package com.moiseum.wolnelektury.components; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.Display; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; + +import it.sephiroth.android.library.imagezoom.ImageViewTouch; + +/** + * Created by Piotr Ostrowski on 29.06.2017. + */ + +public class ZoomableViewPager extends ViewPager { + + public interface OnItemClickListener { + void onItemClick(int position); + } + + private class TapGestureListener extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (mOnItemClickListener != null) { + mOnItemClickListener.onItemClick(getCurrentItem()); + } + return true; + } + } + + private OnItemClickListener mOnItemClickListener; + private GestureDetector tapGestureDetector; + + public ZoomableViewPager(Context context) { + super(context); + setup(); + } + + public ZoomableViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + setup(); + } + + @Override + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ImageViewTouch) { + ImageViewTouch imageViewTouch = (ImageViewTouch) v; + return imageViewTouchCanScroll(imageViewTouch, dx); + } else { + return super.canScroll(v, checkV, dx, x, y); + } + } + + /** + * Determines whether the ImageViewTouch can be scrolled. + * + * @param direction - positive direction value means scroll from right to left, + * negative value means scroll from left to right + * @return true if there is some more place to scroll, false - otherwise. + */ + private boolean imageViewTouchCanScroll(ImageViewTouch imageViewTouch, int direction) { + int widthScreen = getWidthScreen(); + + RectF bitmapRect = imageViewTouch.getBitmapRect(); + Rect imageViewRect = new Rect(); + getGlobalVisibleRect(imageViewRect); + + int widthBitmapViewTouch = (int) bitmapRect.width(); + + if (widthBitmapViewTouch < widthScreen) { + return false; + } + + if (direction < 0) { + return Math.abs(bitmapRect.right - imageViewRect.right) > 1.0f; + } else { + return Math.abs(bitmapRect.left - imageViewRect.left) > 1.0f; + } + + } + + private int getWidthScreen() { + WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + + Point size = new Point(); + display.getSize(size); + return size.x; + } + + private void setup() { + tapGestureDetector = new GestureDetector(getContext(), new TapGestureListener()); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + tapGestureDetector.onTouchEvent(ev); + return super.onInterceptTouchEvent(ev); + } + + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + mOnItemClickListener = onItemClickListener; + } + +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/EndlessRecyclerOnScrollListener.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/EndlessRecyclerOnScrollListener.java new file mode 100644 index 0000000..073dff1 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/EndlessRecyclerOnScrollListener.java @@ -0,0 +1,50 @@ +package com.moiseum.wolnelektury.components.recycler; + +/** + * Created by Piotr Ostrowski on 28.11.2017. + */ + +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + +public abstract class EndlessRecyclerOnScrollListener extends RecyclerView.OnScrollListener { + + public static String TAG = EndlessRecyclerOnScrollListener.class.getSimpleName(); + + /** + * The total number of items in the dataset after the last load + */ + private int mPreviousTotal = 0; + /** + * True if we are still waiting for the last set of data to load. + */ + private boolean mLoading = true; + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + int visibleItemCount = recyclerView.getChildCount(); + int totalItemCount = recyclerView.getLayoutManager().getItemCount(); + int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); + + if (mLoading) { + if (totalItemCount > mPreviousTotal) { + mLoading = false; + mPreviousTotal = totalItemCount; + } + } + int visibleThreshold = 5; + if (!mLoading && (totalItemCount - visibleItemCount) <= (firstVisibleItem + visibleThreshold)) { + // End has been reached + onLoadMore(); + mLoading = true; + } + } + + public void reset() { + mPreviousTotal = 0; + } + + public abstract void onLoadMore(); +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/RecyclerAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/RecyclerAdapter.java new file mode 100644 index 0000000..c2b7137 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/RecyclerAdapter.java @@ -0,0 +1,187 @@ +package com.moiseum.wolnelektury.components.recycler; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.moiseum.wolnelektury.connection.models.CategoryModel; + +import java.util.Collections; +import java.util.List; + +/** + * @author golonkos + */ + +public abstract class RecyclerAdapter extends RecyclerView.Adapter { + + /** + * On click listener. + */ + public interface OnItemClickListener { + /** + * @param item clicked item + * @param view clicked view + */ + void onItemClicked(T item, View view, int position); + } + + public enum Selection { + NONE, SINGLE + } + + private static final int NO_POSITION = -1; + + private LayoutInflater layoutInflater; + private OnItemClickListener onItemClickListener; + + private List items = Collections.emptyList(); + + private T selectedItem; + private int selectedItemPosition = NO_POSITION; + + private Selection selection = Selection.NONE; + + private View.OnClickListener onClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + int position = (int) v.getTag(); + T item = getItem(position); + onItemClicked(v, item, position); + } + }; + + public RecyclerAdapter(Context context, Selection selection) { + layoutInflater = LayoutInflater.from(context); + this.selection = selection; + } + + protected void onItemClicked(View view, T item, int position) { + selectItemAndNotify(item, position); + if (onItemClickListener != null) { + onItemClickListener.onItemClicked(item, view, position); + } + } + + @Override + public void onBindViewHolder(VH viewHolder, final int position) { + viewHolder.itemView.setTag(position); + viewHolder.itemView.setOnClickListener(onClickListener); + T item = getItem(position); + viewHolder.itemView.setSelected(isSelected(getItem(position))); + viewHolder.bind(item, isSelected(item)); + } + + @Override + public int getItemCount() { + return items.size(); + } + + + /** + * @param onItemClickListener item click listener + */ + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + this.onItemClickListener = onItemClickListener; + } + + + public OnItemClickListener getOnItemClickListener() { + return onItemClickListener; + } + + public List getItems() { + return items; + } + + /** + * @param items new items + */ + public void setItems(List items) { + this.items = items; + notifyDataSetChanged(); + } + + public void addItems(List items) { + this.items.addAll(items); + notifyDataSetChanged(); + } + + /** + * @param position position of element to remove + * @return removed item or null if element not found + */ + public T removeItem(int position) { + if (position >= 0 && position < items.size()) { + T item = items.remove(position); + notifyItemRemoved(position); + notifyItemRangeChanged(position, items.size()); + return item; + } + return null; + } + + public void clear() { + this.items.clear(); + notifyDataSetChanged(); + } + + protected void addItem(int position, T item) { + items.add(position, item); + } + + protected View inflate(int layoutResId, ViewGroup parent) { + return layoutInflater.inflate(layoutResId, parent, false); + } + + /** + * @param position position of element + * @return item from specific position + */ + public T getItem(int position) { + return this.items.get(position); + } + + protected abstract String getItemId(T item); + + private boolean isSelected(T item) { + return selectedItem != null && getItemId(selectedItem).contains(getItemId(item)); + } + + public void selectItem(T item) { + int position = NO_POSITION; + for (int i = 0; i < items.size(); i++) { + if (getItemId(item).equals(getItemId(items.get(i)))) { + position = i; + break; + } + } + selectItemAndNotify(item, position); + } + + private void selectItemAndNotify(T item, int position) { + int selectedPositionToNotify = selectedItemPosition; + setSelectedItem(item, position); + + if (selectedPositionToNotify != NO_POSITION) { + notifyItemChanged(selectedPositionToNotify); + } + notifyItemChanged(position); + } + + private void setSelectedItem(T item, int position) { + switch (selection) { + case SINGLE: + selectedItem = item; + selectedItemPosition = position; + break; + case NONE: + break; + } + } + + +} + diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/ViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/ViewHolder.java new file mode 100644 index 0000000..53f2f26 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/ViewHolder.java @@ -0,0 +1,25 @@ +package com.moiseum.wolnelektury.components.recycler; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import butterknife.ButterKnife; + +public abstract class ViewHolder extends RecyclerView.ViewHolder { + + public ViewHolder(View view) { + super(view); + ButterKnife.bind(this, view); + } + + public View getView() { + return itemView; + } + + protected Context getContext() { + return getView().getContext(); + } + + public abstract void bind(T item, boolean selected); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/ErrorHandler.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/ErrorHandler.java new file mode 100644 index 0000000..a14e4fd --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/ErrorHandler.java @@ -0,0 +1,53 @@ +package com.moiseum.wolnelektury.connection; + +import java.io.IOException; + +import retrofit2.Response; + +import static java.net.HttpURLConnection.HTTP_BAD_METHOD; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_FORBIDDEN; +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; + +/** + * @author golonkos. + */ + +public class ErrorHandler { + + private static final String TAG = ErrorHandler.class.getSimpleName(); + private final Response response; + + public ErrorHandler(Response response) { + this.response = response; + } + + public void handle() throws IOException { + // There is no error model returned for this API + switch (response.code()) { + case HTTP_BAD_REQUEST: + case HTTP_NOT_FOUND: + case HTTP_BAD_METHOD: + case HTTP_INTERNAL_ERROR: + case HTTP_FORBIDDEN: + default: + throw new IOException("Unknown or unhandled exception for response " + response.code() + ", " + response.message()); + } + + } + + // public ErrorModel parseError(Response response) { + // try { + // Gson gson = new GsonBuilder().create(); + // return gson.fromJson(response.errorBody().string(), ErrorModel.class); + // } catch (IOException | JsonSyntaxException e) { + // Log.e(TAG, "Error while parsing error json", e); + // return null; + // } + // } + + public int getResponseCode() { + return response.code(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClient.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClient.java new file mode 100644 index 0000000..35eb345 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClient.java @@ -0,0 +1,107 @@ +package com.moiseum.wolnelektury.connection; + +import android.content.Context; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.moiseum.wolnelektury.BuildConfig; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.connection.interceptors.NewApiInterceptor; +import com.moiseum.wolnelektury.connection.interceptors.OAuthSigningInterceptor; +import com.moiseum.wolnelektury.connection.interceptors.UnauthorizedInterceptor; +import com.moiseum.wolnelektury.connection.models.OAuthTokenModel; +import com.moiseum.wolnelektury.connection.services.BooksService; + +import java.io.File; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import okhttp3.Cache; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Call; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; + +/** + * @author golonkos. + */ + +public class RestClient { + public static final int PAGINATION_LIMIT = 30; + + public static final String MEDIA_URL = "http://wolnelektury.pl/media/"; + public static final String MEDIA_URL_HTTPS = "https://wolnelektury.pl/media/"; + public static final String BASE_URL = "https://wolnelektury.pl/api/"; + public static final String WEB_OAUTH_AUTHORIZATION_URL = ""; + public static final String WEB_PAYPAL_FORM_URL = ""; + + private static final String CACHE_DIR = "responses"; + private static final int CACHE_SIZE_MB = 10; + + private static final String CONSUMER_KEY = ""; + private static final String CONSUMER_SECRET = ""; + + private final Retrofit retrofit; + private final OAuthSigningInterceptor oAuthInterceptor; + + public RestClient(Context context) { + OAuthTokenModel currentToken = WLApplication.getInstance().getPreferences().getAccessToken(); + oAuthInterceptor = new OAuthSigningInterceptor(CONSUMER_KEY, CONSUMER_SECRET, new Random()); + if (currentToken != null) { + oAuthInterceptor.setToken(currentToken.getToken(), currentToken.getTokenSecret()); + } + UnauthorizedInterceptor unauthorizedInterceptor = new UnauthorizedInterceptor(); + NewApiInterceptor newApiInterceptor = new NewApiInterceptor(); + + GsonBuilder gsonBuilder = new GsonBuilder(); + //gsonBuilder.registerTypeAdapter(Date.class, new RestClientDateSerializer()); + + Gson gson = gsonBuilder.create(); + + int cacheSize = CACHE_SIZE_MB * 1024 * 1024; + File cacheDir = new File(context.getCacheDir(), CACHE_DIR); + Cache cache = new Cache(cacheDir, cacheSize); + + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + builder.interceptors().add(newApiInterceptor); + builder.interceptors().add(oAuthInterceptor); + builder.interceptors().add(unauthorizedInterceptor); + if (BuildConfig.DEBUG) { + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS); + builder.interceptors().add(loggingInterceptor); + } + builder.writeTimeout(60, TimeUnit.SECONDS); + builder.readTimeout(60, TimeUnit.SECONDS); + builder.connectTimeout(60, TimeUnit.SECONDS); + + OkHttpClient client = builder.cache(cache).build(); + retrofit = new Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(client) + .build(); + } + + public Call call(RestClientCallback restClientCallback, Class clazz) { + S service = createService(clazz); + Call call = restClientCallback.execute(service); + call.enqueue(restClientCallback); + return call; + } + + public S createService(Class clazz) { + return retrofit.create(clazz); + } + + public void clearOAuthTokens() { + oAuthInterceptor.setToken(null, null); + } + + public BooksService obtainBookService() { + return retrofit.create(BooksService.class); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClientCallback.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClientCallback.java new file mode 100644 index 0000000..41a1ad5 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClientCallback.java @@ -0,0 +1,44 @@ +package com.moiseum.wolnelektury.connection; + +import android.util.Log; + +import retrofit2.Call; +import retrofit2.Response; + +public abstract class RestClientCallback implements retrofit2.Callback { + + private static final String TAG = RestClientCallback.class.getSimpleName(); + + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + onSuccess(response.body()); + } else { + try { + ErrorHandler errorHandler = new ErrorHandler<>(response); + errorHandler.handle(); + } catch (Exception e) { + onFailure(e); + } + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, t.getMessage(), t); + if (!call.isCanceled()) { + onFailure(new Exception(t)); + } else { + onCancel(); + } + } + + + public abstract void onSuccess(T data); + + public abstract void onFailure(Exception e); + + public abstract void onCancel(); + + public abstract Call execute(S service); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/WolneLekturyFirebaseMessagingService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/WolneLekturyFirebaseMessagingService.java new file mode 100644 index 0000000..7a27db5 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/WolneLekturyFirebaseMessagingService.java @@ -0,0 +1,105 @@ +package com.moiseum.wolnelektury.connection; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.RingtoneManager; +import android.net.Uri; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; +import android.util.Log; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; +import com.moiseum.wolnelektury.BuildConfig; +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; +import com.moiseum.wolnelektury.view.main.MainActivity; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import timber.log.Timber; + +/** + * Created by Piotr Ostrowski on 27.08.2018. + */ +public class WolneLekturyFirebaseMessagingService extends FirebaseMessagingService { + + private static final String TAG = WolneLekturyFirebaseMessagingService.class.getSimpleName(); + private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + + @Override + public void onNewToken(String s) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Refreshed token: " + s); + } + FirebaseMessaging.getInstance().subscribeToTopic(getString(R.string.default_notification_topic)); + } + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + Log.d(TAG, "Received notification " + remoteMessage); + if (preferences.getNotifications()) { + String title = remoteMessage.getData().get("title"); + String body = remoteMessage.getData().get("body"); + String imageUrl = remoteMessage.getData().get("imageUrl"); + Bitmap bitmap = getBitmapFromUrl(imageUrl); + sendNotification(title, body, bitmap); + } else { + Log.d(TAG, "Skipping notification cause of user preference"); + } + } + + private void sendNotification(String title, String body, Bitmap image) { + MainActivity.MainIntent intent = new MainActivity.MainIntent(R.string.app_name, this); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT); + Uri defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, getString(R.string.default_notification_channel_id)) + .setLargeIcon(image) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(title) + .setContentText(body) + .setColor(ContextCompat.getColor(this, R.color.colorAccent)) + .setColorized(true) + .setStyle(new NotificationCompat.BigPictureStyle() + .bigPicture(image) + .setBigContentTitle(title) + .setSummaryText(body) + ) + .setAutoCancel(true) + .setSound(defaultUri) + .setContentIntent(pendingIntent); + + if (notificationManager != null) { + notificationManager.notify(0, notificationBuilder.build()); + } + } + + /* + *To get a Bitmap image from the URL received + * */ + private Bitmap getBitmapFromUrl(String imageUrl) { + if (imageUrl == null) { + return null; + } + try { + URL url = new URL(imageUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoInput(true); + connection.connect(); + InputStream input = connection.getInputStream(); + return BitmapFactory.decodeStream(input); + } catch (Exception e) { + Timber.tag(TAG).e(e, "Failed to fetch notification image " + e.getMessage()); + return null; + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileCacheUtils.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileCacheUtils.java new file mode 100644 index 0000000..dc14253 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileCacheUtils.java @@ -0,0 +1,180 @@ +package com.moiseum.wolnelektury.connection.downloads; + +import android.os.Environment; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.connection.models.MediaModel; + +import org.greenrobot.eventbus.EventBus; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +import io.reactivex.Completable; +import okhttp3.ResponseBody; + +import static android.os.Environment.isExternalStorageRemovable; + +/** + * Created by Piotr Ostrowski on 10.05.2017. + */ + +public class FileCacheUtils { + + private static final String FILES_CACHE = "FilesCache"; + private static final int BUFFER_SIZE = 4096; + private static final long PROGRESS_UPDATE_RATE = 10; + private static final String TAG = FileCacheUtils.class.getSimpleName(); + + private FileCacheUtils() { + // nop. + } + + public static String getCachedFileForUrl(String url) { + File cachedFile = new File(getCurrentCachePath() + File.separator + url.hashCode()); + if (cachedFile.exists()) { + return cachedFile.getAbsolutePath(); + } else { + return null; + } + } + + public static boolean deleteFileForUrl(String url) { + File cachedFile = new File(getCurrentCachePath() + File.separator + url.hashCode()); + if (cachedFile.exists()) { + return cachedFile.delete(); + } else { + Log.e(TAG, "There is no file to be removed: " + url); + return false; + } + } + + public static boolean writeResponseBodyToDiskCache(ResponseBody body, String fileUrl) { + try { + String cachePath = getCurrentCachePath(); + File fileCacheDir = new File(cachePath); + File downloadFile = new File(cachePath + File.separator + fileUrl.hashCode() + ".download"); + createCacheFolderAndFile(cachePath, fileCacheDir, downloadFile); + + InputStream inputStream = null; + OutputStream outputStream = null; + + try { + byte[] fileReader = new byte[BUFFER_SIZE]; + long fileSize = body.contentLength(); + long fileSizeDownloaded = 0; + + inputStream = body.byteStream(); + outputStream = new FileOutputStream(downloadFile); + + int updateRate = 0; + while (true) { + int read = inputStream.read(fileReader); + if (read == -1) { + break; + } + + outputStream.write(fileReader, 0, read); + fileSizeDownloaded += read; + if (updateRate++ % PROGRESS_UPDATE_RATE == 0) { + EventBus.getDefault().post(new DownloadProgressEvent(fileUrl, fileSizeDownloaded, fileSize)); + } + } + outputStream.flush(); + + File audioFileDest = new File(cachePath + File.separator + fileUrl.hashCode()); + boolean renamed = downloadFile.renameTo(audioFileDest); + if (!renamed) { + throw new IOException("Failed to rename downloaded file: " + audioFileDest.getAbsolutePath()); + } + return true; + } catch (IOException e) { + Log.e(TAG, "Failed to save file to cache: " + fileUrl, e); + downloadFile.delete(); + return false; + } finally { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + } + } catch (IOException e) { + Log.e(TAG, "File creation or streaming closure failed", e); + return false; + } + } + + private static void createCacheFolderAndFile(String cachePath, File audioCacheDir, File audioFile) throws IOException { + if (!audioCacheDir.exists()) { + boolean result = audioCacheDir.mkdir(); + if (!result) { + throw new IOException("Failed to create AudioCache Dir."); + } + } + if (!audioFile.exists()) { + boolean result = audioFile.createNewFile(); + if (!result) { + throw new IOException("Failed to create file in path: " + audioFile.getAbsolutePath()); + } + } + } + + @NonNull + private static String getCurrentCachePath() { + File externalCacheDir = WLApplication.getInstance().getApplicationContext().getExternalCacheDir(); + String cachePath = (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable()) && + externalCacheDir != null ? externalCacheDir.getPath() : WLApplication.getInstance().getCacheDir().getPath(); + return cachePath + File.separator + FILES_CACHE; + } + + public static Completable deleteAudiobookFiles(List fileUrls) { + return Completable.fromAction(() -> { + for (String fileUrl : fileUrls) { + boolean deleted = deleteFileForUrl(fileUrl); + if (!deleted) { + Log.e(TAG, "Failed to delete file " + FileCacheUtils.getCachedFileForUrl(fileUrl)); + } + } + }); + } + + public static Completable deleteEbookFile(final String epubUrl) { + return Completable.fromAction(() -> deleteFileForUrl(epubUrl)); + } + + /** + * Event indicating progress. + */ + public static class DownloadProgressEvent { + + private String fileUrl; + private long downloaded; + private long total; + + public DownloadProgressEvent(String fileUrl, long downloaded, long total) { + this.fileUrl = fileUrl; + this.downloaded = downloaded; + this.total = total; + } + + public String getFileUrl() { + return fileUrl; + } + + public long getDownloaded() { + return downloaded; + } + + public long getTotal() { + return total; + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileDownloadIntentService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileDownloadIntentService.java new file mode 100644 index 0000000..742f0c7 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileDownloadIntentService.java @@ -0,0 +1,126 @@ +package com.moiseum.wolnelektury.connection.downloads; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.connection.ErrorHandler; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.services.BooksService; + +import org.greenrobot.eventbus.EventBus; +import org.parceler.Parcels; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; + +/** + * Created by piotrostrowski on 07.05.2017. + */ + +public class FileDownloadIntentService extends IntentService { + + private static final String TAG = FileDownloadIntentService.class.getSimpleName(); + public static final String FILE_URL_KEY = "FileUrlKey"; + public static final String FILES_URLS_KEY = "FilesUrlsKey"; + + public static void downloadFile(Context context, String fileUrl) { + Intent downloadIntent = new Intent(context, FileDownloadIntentService.class); + downloadIntent.putExtra(FILE_URL_KEY, fileUrl); + context.startService(downloadIntent); + } + + public static void downloadFiles(Context context, ArrayList filesUrls) { + Intent downloadIntent = new Intent(context, FileDownloadIntentService.class); + downloadIntent.putExtra(FILES_URLS_KEY, Parcels.wrap(filesUrls)); + context.startService(downloadIntent); + } + + public FileDownloadIntentService() { + super(TAG); + } + + @Override + public void onCreate() { + super.onCreate(); + } + + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + if (intent == null) { + return; + } + + if (intent.hasExtra(FILE_URL_KEY)) { + String fileUrl = intent.getStringExtra(FILE_URL_KEY); + checkCacheAndDownload(fileUrl); + } else if (intent.hasExtra(FILES_URLS_KEY)) { + ArrayList filesUrls = Parcels.unwrap(intent.getParcelableExtra(FILES_URLS_KEY)); + for (String fileUrl : filesUrls) { + if (!checkCacheAndDownload(fileUrl)) { + break; + } + } + } + } + + private boolean checkCacheAndDownload(String fileUrl) { + if (FileCacheUtils.getCachedFileForUrl(fileUrl) != null) { + Log.v(TAG, fileUrl + " is already in cache."); + EventBus.getDefault().post(new DownloadFileEvent(fileUrl, true)); + return true; + } + return downloadFile(fileUrl); + } + + private boolean downloadFile(String fileUrl) { + RestClient client = WLApplication.getInstance().getRestClient(); + BooksService booksService = client.createService(BooksService.class); + try { + Call call = booksService.downloadFileWithUrl(fileUrl); + Response response = call.execute(); + if (response.isSuccessful()) { + boolean result = FileCacheUtils.writeResponseBodyToDiskCache(response.body(), fileUrl); + EventBus.getDefault().post(new DownloadFileEvent(fileUrl, result)); + } else { + ErrorHandler errorHandler = new ErrorHandler<>(response); + errorHandler.handle(); + //if nothing cause, throw exception + throw new UnsupportedOperationException("Unhandled exception"); + } + } catch (IOException e) { + Log.e(TAG, "Failed to download audio file: " + fileUrl, e); + EventBus.getDefault().post(new DownloadFileEvent(fileUrl, false)); + return false; + } + return true; + } + + public static class DownloadFileEvent { + + private String fileUrl; + private boolean success; + + DownloadFileEvent(String fileUrl, boolean success) { + this.fileUrl = fileUrl; + this.success = success; + } + + public String getFileUrl() { + return fileUrl; + } + + public boolean isSuccess() { + return success; + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/NewApiInterceptor.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/NewApiInterceptor.java new file mode 100644 index 0000000..2e874eb --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/NewApiInterceptor.java @@ -0,0 +1,37 @@ +package com.moiseum.wolnelektury.connection.interceptors; + +import android.support.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Created by Piotr Ostrowski on 24.09.2018. + */ +public class NewApiInterceptor implements Interceptor { + + private static final String NEW_API_HEADER = "New-Api"; + private static final String NEW_API_PARAM = "new_api"; + + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + if (chain.request().header(NEW_API_HEADER) != null) { + HttpUrl httpUrl = chain.request() + .url() + .newBuilder() + .addQueryParameter(NEW_API_PARAM, Boolean.toString(true)) + .build(); + Request newRequest = chain.request() + .newBuilder() + .url(httpUrl) + .build(); + return chain.proceed(newRequest); + } + + return chain.proceed(chain.request()); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/OAuthSigningInterceptor.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/OAuthSigningInterceptor.java new file mode 100644 index 0000000..0fc1092 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/OAuthSigningInterceptor.java @@ -0,0 +1,245 @@ +package com.moiseum.wolnelektury.connection.interceptors; + +/** + * Created by Piotr Ostrowski on 06.06.2018. + */ +/* + * Copyright (C) 2015 Jake Wharton + * + * 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.support.annotation.NonNull; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.moiseum.wolnelektury.connection.models.OAuthTokenModel; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.Map; +import java.util.Random; +import java.util.SortedMap; +import java.util.TreeMap; + +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.ByteString; + +public final class OAuthSigningInterceptor implements Interceptor { + private static final String TAG = OAuthSigningInterceptor.class.getSimpleName(); + private static final String REQUEST_TOKEN_HEADER = "Token-Requested"; + private static final String AUTH_REQUIRED_HEADER = "Authentication-Required"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + + private static final String OAUTH_REALM = "realm=\"API\", "; + private static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key"; + private static final String OAUTH_NONCE = "oauth_nonce"; + private static final String OAUTH_SIGNATURE = "oauth_signature"; + private static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method"; + private static final String OAUTH_SIGNATURE_METHOD_VALUE = "HMAC-SHA1"; + private static final String OAUTH_TIMESTAMP = "oauth_timestamp"; + private static final String OAUTH_ACCESS_TOKEN = "oauth_token"; + private static final String OAUTH_VERSION = "oauth_version"; + private static final String OAUTH_VERSION_VALUE = "1.0"; + private static final long ONE_SECOND = 1000; + + private final String consumerKey; + private final String consumerSecret; + private final Random random; + private String accessToken; + private String accessSecret; + + public OAuthSigningInterceptor(String consumerKey, String consumerSecret, Random random) { + this.consumerKey = consumerKey; + this.consumerSecret = consumerSecret; + this.random = random; + } + + public void setToken(String accessToken, String accessSecret) { + this.accessToken = accessToken; + this.accessSecret = accessSecret; + } + + @Override + public Response intercept(Chain chain) throws IOException { + if (chain.request().header(REQUEST_TOKEN_HEADER) != null) { + return handleRequestTokenRequest(chain); + } else if (chain.request().header(AUTH_REQUIRED_HEADER) != null || isSignedIn()) { + return chain.proceed(signRequest(chain.request())); + } else { + return chain.proceed(chain.request()); + } + } + + private boolean isSignedIn() { + return accessSecret != null && accessToken != null; + } + + private Response handleRequestTokenRequest(Chain chain) throws IOException { + Response tokenResponse = chain.proceed(requestTokenRequest(chain.request())); + if (tokenResponse.isSuccessful() && tokenResponse.code() == 200 && tokenResponse.body() != null) { + String jsonResponse = paramJson(tokenResponse.body().string()); + try { + Gson gson = new Gson(); + OAuthTokenModel tokenModel = gson.fromJson(jsonResponse, OAuthTokenModel.class); + accessToken = tokenModel.getToken(); + accessSecret = tokenModel.getTokenSecret(); + return tokenResponse.newBuilder().body(ResponseBody.create(MediaType.parse("application/json"), jsonResponse)).build(); + } catch (JsonSyntaxException e) { + Log.v(TAG, "Failed to parse Oauth Request Token response.", e); + } + } + return tokenResponse; + } + + private Request signRequest(Request request) throws IOException { + if (accessToken == null || accessSecret == null) { + Log.e(TAG, "Missing authentication tokens, passing request unsigned."); + return request; + } + + SortedMap parameters = getOAuthParams(request.url(), request.body()); + + String baseUrl = request.url().newBuilder().query(null).build().toString(); + ByteString baseString = getBaseString(request.method(), baseUrl, parameters); + String signingKey = utf8(consumerSecret) + "&" + (accessSecret != null ? utf8(accessSecret) : ""); + String signature = baseString.hmacSha1(ByteString.of(signingKey.getBytes())).base64(); + + String authorization = "OAuth " + OAUTH_REALM + + OAUTH_CONSUMER_KEY + "=\"" + parameters.get(OAUTH_CONSUMER_KEY) + "\", " + + OAUTH_NONCE + "=\"" + parameters.get(OAUTH_NONCE) + "\", " + + OAUTH_SIGNATURE + "=\"" + signature + "\", " + + OAUTH_SIGNATURE_METHOD + "=\"" + OAUTH_SIGNATURE_METHOD_VALUE + "\", " + + OAUTH_TIMESTAMP + "=\"" + parameters.get(OAUTH_TIMESTAMP) + "\", " + + OAUTH_ACCESS_TOKEN + "=\"" + accessToken + "\", " + + OAUTH_VERSION + "=\"" + OAUTH_VERSION_VALUE + "\""; + + return request.newBuilder() + .addHeader(AUTHORIZATION_HEADER, authorization) + .build(); + } + + private Request requestTokenRequest(Request request) throws IOException { + SortedMap parameters = getOAuthParams(request.url(), request.body()); + + String baseUrl = request.url().newBuilder().query(null).build().toString(); + ByteString baseString = getBaseString(request.method(), baseUrl, parameters); + String signingKey = utf8(consumerSecret) + "&" + (accessSecret != null ? utf8(accessSecret) : ""); + String signature = baseString.hmacSha1(ByteString.of(signingKey.getBytes())).base64(); + + HttpUrl.Builder urlBuilder = request.url().newBuilder() + .addQueryParameter(OAUTH_CONSUMER_KEY, parameters.get(OAUTH_CONSUMER_KEY)) + .addQueryParameter(OAUTH_NONCE, parameters.get(OAUTH_NONCE)) + .addQueryParameter(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE) + .addQueryParameter(OAUTH_TIMESTAMP, parameters.get(OAUTH_TIMESTAMP)) + .addQueryParameter(OAUTH_VERSION, OAUTH_VERSION_VALUE) + .addQueryParameter(OAUTH_SIGNATURE, signature); + if (accessToken != null) { + urlBuilder.addQueryParameter(OAUTH_ACCESS_TOKEN, accessToken); + } + HttpUrl requestUrl = urlBuilder.build(); + + return request.newBuilder().url(requestUrl).build(); + } + + private SortedMap getOAuthParams(HttpUrl url, RequestBody requestBody) throws IOException { + byte[] nonce = new byte[32]; + random.nextBytes(nonce); + + String oauthNonce = ByteString.of(nonce).base64().replaceAll("\\W", ""); + String oauthTimestamp = String.valueOf(System.currentTimeMillis() / ONE_SECOND); + + SortedMap parameters = new TreeMap<>(); + parameters.put(OAUTH_CONSUMER_KEY, utf8(consumerKey)); + parameters.put(OAUTH_NONCE, oauthNonce); + parameters.put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE); + parameters.put(OAUTH_TIMESTAMP, oauthTimestamp); + parameters.put(OAUTH_VERSION, OAUTH_VERSION_VALUE); + if (accessToken != null) { + parameters.put(OAUTH_ACCESS_TOKEN, accessToken); + } + + // Adding query params + for (int i = 0; i < url.querySize(); i++) { + parameters.put(utf8(url.queryParameterName(i)), utf8(url.queryParameterValue(i))); + } + + // Adding body params + if (requestBody != null) { + Buffer body = new Buffer(); + requestBody.writeTo(body); + + while (!body.exhausted()) { + long keyEnd = body.indexOf((byte) '='); + if (keyEnd == -1) { + throw new IllegalStateException("Key with no value: " + body.readUtf8()); + } + String key = body.readUtf8(keyEnd); + body.skip(1); // Equals. + + long valueEnd = body.indexOf((byte) '&'); + String value = valueEnd == -1 ? body.readUtf8() : body.readUtf8(valueEnd); + if (valueEnd != -1) { + body.skip(1); // Ampersand. + } + + parameters.put(key, value); + } + } + + return parameters; + } + + @NonNull + private ByteString getBaseString(String method, String baseUrl, SortedMap parameters) throws IOException { + Buffer base = new Buffer(); + base.writeUtf8(method); + base.writeByte('&'); + base.writeUtf8(utf8(baseUrl)); + base.writeByte('&'); + + boolean first = true; + for (Map.Entry entry : parameters.entrySet()) { + if (!first) { + base.writeUtf8(utf8("&")); + } + first = false; + base.writeUtf8(utf8(entry.getKey())); + base.writeUtf8(utf8("=")); + base.writeUtf8(utf8(entry.getValue())); + } + return ByteString.of(base.readByteArray()); + } + + private String utf8(String escapedString) throws IOException { + return URLEncoder.encode(escapedString, "UTF-8") + .replace("+", "%20") + .replace("*", "%2A") + .replace("%7E", "~"); + } + + private String paramJson(String paramIn) { + paramIn = paramIn.replaceAll("=", "\":\""); + paramIn = paramIn.replaceAll("&", "\",\""); + return "{\"" + paramIn + "\"}"; + } + +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/UnauthorizedInterceptor.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/UnauthorizedInterceptor.java new file mode 100644 index 0000000..91dc357 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/UnauthorizedInterceptor.java @@ -0,0 +1,40 @@ +package com.moiseum.wolnelektury.connection.interceptors; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.view.main.MainActivity; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import okhttp3.Interceptor; +import okhttp3.Response; + +/** + * Created by Piotr Ostrowski on 23.06.2018. + */ +public class UnauthorizedInterceptor implements Interceptor { + + private static final String TAG = UnauthorizedInterceptor.class.getSimpleName(); + + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + Response response = chain.proceed(chain.request()); + if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + Log.e(TAG, "Provided credentials were invalid. Re-launching app"); + + WLApplication.getInstance().getPreferences().clearUserData(); + WLApplication.getInstance().getRestClient().clearOAuthTokens(); + + Context context = WLApplication.getInstance().getApplicationContext(); + MainActivity.MainIntent intent = new MainActivity.MainIntent(R.string.unauthorized, context); + context.startActivity(intent); + } + return response; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookDetailsModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookDetailsModel.java new file mode 100644 index 0000000..0a2959e --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookDetailsModel.java @@ -0,0 +1,237 @@ +package com.moiseum.wolnelektury.connection.models; + +import com.google.gson.annotations.SerializedName; +import com.moiseum.wolnelektury.utils.StringUtils; + +import org.parceler.Parcel; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by piotrostrowski on 17.11.2017. + */ + +@Parcel(Parcel.Serialization.BEAN) +public class BookDetailsModel { + + private static final String HREF_BASE = "http://wolnelektury.pl/api/books/"; + private static final String MEDIA_TYPE_MP3 = "mp3"; + + private List genres; + private List kinds; + private BookModel parent; + private String title; + private String url; + private List media; + @SerializedName("simple_cover") + private String cover; + private List epochs; + private List authors; + private String pdf; + private String epub; + @SerializedName("simple_thumb") + private String coverThumb; + @SerializedName("fragment_data") + private FragmentModel fragment; + @SerializedName("audio_length") + private String audioLength; + private ReadingStateModel.ReadingState state; + private Boolean favouriteState; + @SerializedName("cover_color") + private String coverColor; + private String slug; + + public BookDetailsModel() { + } + + public List getGenres() { + return genres; + } + + public void setGenres(List genres) { + this.genres = genres; + } + + public List getKinds() { + return kinds; + } + + public void setKinds(List kinds) { + this.kinds = kinds; + } + + public BookModel getParent() { + return parent; + } + + public void setParent(BookModel parent) { + this.parent = parent; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public List getMedia() { + return media; + } + + public void setMedia(List media) { + this.media = media; + } + + public String getCover() { + return cover; + } + + public void setCover(String cover) { + this.cover = cover; + } + + public List getEpochs() { + return epochs; + } + + public void setEpochs(List epochs) { + this.epochs = epochs; + } + + public List getAuthors() { + return authors; + } + + public void setAuthors(List authors) { + this.authors = authors; + } + + public String getPdf() { + return pdf; + } + + public void setPdf(String pdf) { + this.pdf = pdf; + } + + public String getEpub() { + return epub; + } + + public void setEpub(String epub) { + this.epub = epub; + } + + public String getCoverThumb() { + return coverThumb; + } + + public void setCoverThumb(String coverThumb) { + this.coverThumb = coverThumb; + } + + public String getCoverColor() { + return coverColor; + } + + public void setCoverColor(String coverColor) { + this.coverColor = coverColor; + } + + public FragmentModel getFragment() { + return fragment; + } + + public void setFragment(FragmentModel fragment) { + this.fragment = fragment; + } + + public String getAudioLength() { + return audioLength; + } + + public void setAudioLength(String audioLength) { + this.audioLength = audioLength; + } + + public boolean hasAudio() { + return media != null && media.size() > 0; + } + + public ReadingStateModel.ReadingState getState() { + return state; + } + + public void setState(ReadingStateModel.ReadingState state) { + this.state = state; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public BookModel getStorageModel(String slug) { + BookModel model = new BookModel(); + model.setAuthor(StringUtils.joinCategory(getAuthors(), ", ")); + model.setCover(getCover()); + model.setCoverColor(getCoverColor()); + model.setCoverThumb(getCoverThumb()); + model.setEpoch(StringUtils.joinCategory(getEpochs(), ", ")); + model.setGenre(StringUtils.joinCategory(getGenres(), ", ")); + model.setKind(StringUtils.joinCategory(getKinds(), ", ")); + model.setHref(HREF_BASE + slug + "/"); + model.setTitle(getTitle()); + model.setSlug(slug); + model.setUrl(getUrl()); + model.setHasAudio(hasAudio()); + return model; + } + + public ArrayList getAudiobookMediaModels() { + ArrayList mediaModels = new ArrayList<>(); + for (MediaModel mediaFile : getMedia()) { + if (MEDIA_TYPE_MP3.equals(mediaFile.getType())) { + mediaModels.add(mediaFile); + } + } + return mediaModels; + } + + public ArrayList getAudiobookFilesUrls() { + ArrayList urls = new ArrayList<>(); + for (MediaModel mediaFile : getMedia()) { + if (MEDIA_TYPE_MP3.equals(mediaFile.getType())) { + urls.add(mediaFile.getUrl()); + } + } + return urls; + } + + public boolean getFavouriteState() { + return favouriteState; + } + + public void setFavouriteState(boolean favouriteState) { + this.favouriteState = favouriteState; + } + + public String getFavouriteString(boolean favouriteState) { + return favouriteState ? "like" : "unlike"; + } +} + diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookModel.java new file mode 100644 index 0000000..d5b8519 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookModel.java @@ -0,0 +1,246 @@ +package com.moiseum.wolnelektury.connection.models; + +import com.google.gson.annotations.SerializedName; +import com.moiseum.wolnelektury.storage.StringListConverter; + +import org.parceler.Parcel; + +import java.util.List; + +import io.objectbox.annotation.Convert; +import io.objectbox.annotation.Entity; +import io.objectbox.annotation.Id; + +/** + * Created by piotrostrowski on 16.11.2017. + */ + +@Parcel(Parcel.Serialization.BEAN) +@Entity +public class BookModel { + + @Id(assignable = true) + private long localId; + + // API provided fields + private String kind; + private String author; + private String url; + @SerializedName("has_audio") + private boolean hasAudio; + private String title; + private String cover; + private String epoch; + private String href; + private String genre; + private String slug; + @SerializedName("cover_color") + private String coverColor; + private String key; + @SerializedName("full_sort_key") + private String sortedKey; + @SerializedName("simple_thumb") + private String coverThumb; + private boolean liked; + + // Locally stored fields + private String ebookName; + private int currentChapter; + private int totalChapters; + private String ebookFileUrl; + private int currentAudioChapter; + private int totalAudioChapters; + @Convert(converter = StringListConverter.class, dbType = String.class) + private List audioFileUrls; + + public BookModel() { + } + + public long getLocalId() { + return localId; + } + + public void setLocalId(long localId) { + this.localId = localId; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public boolean isHasAudio() { + return hasAudio; + } + + public void setHasAudio(boolean hasAudio) { + this.hasAudio = hasAudio; + } + + public String getCover() { + return cover; + } + + public void setCover(String cover) { + this.cover = cover; + } + + public String getEpoch() { + return epoch; + } + + public void setEpoch(String epoch) { + this.epoch = epoch; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + + public String getSlug() { + return slug; + } + + public String getCoverColor(){return coverColor;} + + public void setCoverColor(String coverColor){this.coverColor=coverColor;} + + public void setSlug(String slug) { + this.slug = slug; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getSortedKey() { + return sortedKey; + } + + public void setSortedKey(String sortedKey) { + this.sortedKey = sortedKey; + } + + public String getCoverThumb() { + return coverThumb; + } + + public void setCoverThumb(String coverThumb) { + this.coverThumb = coverThumb; + } + + public String getEbookName() { + return ebookName; + } + + public void setEbookName(String ebookName) { + this.ebookName = ebookName; + } + + public int getCurrentChapter() { + return currentChapter; + } + + public void setCurrentChapter(int currentChapter) { + this.currentChapter = currentChapter; + } + + public int getTotalChapters() { + return totalChapters; + } + + public void setTotalChapters(int totalChapters) { + this.totalChapters = totalChapters; + } + + public String getEbookFileUrl() { + return ebookFileUrl; + } + + public void setEbookFileUrl(String ebookFileUrl) { + this.ebookFileUrl = ebookFileUrl; + } + + public int getCurrentAudioChapter() { + return currentAudioChapter; + } + + public void setCurrentAudioChapter(int currentAudioChapter) { + this.currentAudioChapter = currentAudioChapter; + } + + public int getTotalAudioChapters() { + return totalAudioChapters; + } + + public void setTotalAudioChapters(int totalAudioChapters) { + this.totalAudioChapters = totalAudioChapters; + } + + public List getAudioFileUrls() { + return audioFileUrls; + } + + public void setAudioFileUrls(List audioFileUrls) { + this.audioFileUrls = audioFileUrls; + } + + public boolean isEbookDownloaded() { + return ebookFileUrl != null; + } + + public boolean isAudioDownloaded() { + return audioFileUrls != null && audioFileUrls.size() > 0; + } + + public boolean isDeletable() { + return ebookFileUrl != null || (audioFileUrls != null && audioFileUrls.size() > 0); + } + + public boolean isLiked() { return liked; } + + public void setLiked(boolean liked) { this.liked = liked; } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/CategoryModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/CategoryModel.java new file mode 100644 index 0000000..a4bf7da --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/CategoryModel.java @@ -0,0 +1,61 @@ +package com.moiseum.wolnelektury.connection.models; + +import org.parceler.Parcel; + +/** + * Created by piotrostrowski on 17.11.2017. + */ + +@Parcel(Parcel.Serialization.BEAN) +public class CategoryModel { + + private String url; + private String href; + private String name; + private String slug; + private boolean checked; + + public CategoryModel() { + + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public boolean isChecked() { + return checked; + } + + public void setChecked(boolean checked) { + this.checked = checked; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FavouriteStateModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FavouriteStateModel.java new file mode 100644 index 0000000..9058aab --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FavouriteStateModel.java @@ -0,0 +1,17 @@ +package com.moiseum.wolnelektury.connection.models; + +import com.google.gson.annotations.SerializedName; + +public class FavouriteStateModel { + + @SerializedName("likes") + private boolean state; + + public FavouriteStateModel() { + this.state = false; + } + + public boolean getState() { + return state; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FragmentModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FragmentModel.java new file mode 100644 index 0000000..6c4c866 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FragmentModel.java @@ -0,0 +1,32 @@ +package com.moiseum.wolnelektury.connection.models; + +import org.parceler.Parcel; + +/** + * Created by piotrostrowski on 30.11.2017. + */ +@Parcel(Parcel.Serialization.BEAN) +public class FragmentModel { + + private String html; + private String title; + + public FragmentModel() { + } + + public String getHtml() { + return html; + } + + public void setHtml(String html) { + this.html = html; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/MediaModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/MediaModel.java new file mode 100644 index 0000000..8c78e46 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/MediaModel.java @@ -0,0 +1,60 @@ +package com.moiseum.wolnelektury.connection.models; + +import org.parceler.Parcel; + +/** + * Created by piotrostrowski on 17.11.2017. + */ + +@Parcel(Parcel.Serialization.BEAN) +public class MediaModel { + + private String url; + private String director; + private String type; + private String name; + private String artist; + + public MediaModel() { + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDirector() { + return director; + } + + public void setDirector(String director) { + this.director = director; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/NewsModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/NewsModel.java new file mode 100644 index 0000000..f4d7479 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/NewsModel.java @@ -0,0 +1,118 @@ +package com.moiseum.wolnelektury.connection.models; + +import com.google.gson.annotations.SerializedName; + +import org.parceler.Parcel; + +import java.util.List; + +@Parcel(Parcel.Serialization.BEAN) +public class NewsModel { + + private String body; + private String lead; + private String title; + private String url; + @SerializedName("image_url") + private String imageUrl; + private String key; + private String time; + private String place; + @SerializedName("image_thumb") + private String thumbUrl; + @SerializedName("gallery_urls") + private List galleryUrl; + private String type; + + public NewsModel() { + + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getLead() { + return lead; + } + + public void setLead(String lead) { + this.lead = lead; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + } + + public String getPlace() { + return place; + } + + public void setPlace(String place) { + this.place = place; + } + + public String getThumbUrl() { + return thumbUrl; + } + + public void setThumbUrl(String thumbUrl) { + this.thumbUrl = thumbUrl; + } + + public List getGalleryUrl() { + return galleryUrl; + } + + public void setGalleryUrl(List galleryUrl) { + this.galleryUrl = galleryUrl; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/OAuthTokenModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/OAuthTokenModel.java new file mode 100644 index 0000000..778ee51 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/OAuthTokenModel.java @@ -0,0 +1,36 @@ +package com.moiseum.wolnelektury.connection.models; + +import com.google.gson.annotations.SerializedName; + +import org.parceler.Parcel; + +/** + * Created by Piotr Ostrowski on 11.06.2018. + */ +@Parcel(Parcel.Serialization.BEAN) +public class OAuthTokenModel { + + @SerializedName("oauth_token_secret") + private String tokenSecret; + @SerializedName("oauth_token") + private String token; + + public OAuthTokenModel() { + } + + public String getTokenSecret() { + return tokenSecret; + } + + public void setTokenSecret(String tokenSecret) { + this.tokenSecret = tokenSecret; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/ReadingStateModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/ReadingStateModel.java new file mode 100644 index 0000000..698c6d8 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/ReadingStateModel.java @@ -0,0 +1,61 @@ +package com.moiseum.wolnelektury.connection.models; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by Piotr Ostrowski on 23.06.2018. + */ +public class ReadingStateModel { + + private static final String UNKNOWN = "unknown"; + private static final String NOT_STARTED = "not_started"; + private static final String READING = "reading"; + private static final String COMPLETED = "complete"; + + public enum ReadingState { + @SerializedName(UNKNOWN) + STATE_UNKNOWN { + @Override + public String getStateName() { + return UNKNOWN; + } + }, + @SerializedName(NOT_STARTED) + STATE_NOT_STARTED { + @Override + public String getStateName() { + return NOT_STARTED; + } + }, + @SerializedName(READING) + STATE_READING { + @Override + public String getStateName() { + return READING; + } + }, + @SerializedName(COMPLETED) + STATE_COMPLETED { + @Override + public String getStateName() { + return COMPLETED; + } + }; + + public abstract String getStateName(); + } + + private ReadingState state; + + public ReadingStateModel() { + this.state = ReadingState.STATE_UNKNOWN; + } + + public ReadingState getState() { + return state; + } + + public void setState(ReadingState state) { + this.state = state; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/UserModel.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/UserModel.java new file mode 100644 index 0000000..7f5c6d1 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/UserModel.java @@ -0,0 +1,25 @@ +package com.moiseum.wolnelektury.connection.models; + +/** + * Created by Piotr Ostrowski on 21.06.2018. + */ +public class UserModel { + private String username; + private boolean premium; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public boolean isPremium() { + return premium; + } + + public void setPremium(boolean premium) { + this.premium = premium; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/BooksService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/BooksService.java new file mode 100644 index 0000000..1657930 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/BooksService.java @@ -0,0 +1,81 @@ +package com.moiseum.wolnelektury.connection.services; + +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.FavouriteStateModel; +import com.moiseum.wolnelektury.connection.models.ReadingStateModel; + +import java.util.List; + +import io.reactivex.Single; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Headers; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; +import retrofit2.http.Streaming; +import retrofit2.http.Url; + +/** + * Created by Piotr Ostrowski on 16.11.2017. + */ + +public interface BooksService { + + @Headers("New-Api: true") + @GET("filter-books/") + Call> getSearchBooks(@Query("search") String search, @Query("epochs") String epochs, @Query("genres") String genres, @Query("kinds") String kinds, + @Query("audiobook") Boolean audiobook, @Query("lektura") Boolean lecture, @Query("after") String lastKey, @Query("count") int count); + + @GET("books/{slug}") + Single getBookDetails(@Path("slug") String slug); + + @Streaming + @GET + Call downloadFileWithUrl(@Url String fileUrl); + + @Headers("New-Api: true") + @GET("newest/") + Call> getNewest(); + + @Headers("New-Api: true") + @GET("recommended/") + Call> getRecommended(); + + @Headers("New-Api: true") + @GET("audiobooks/") + Call> getAudiobooks(@Query("after") String lastKey, @Query("count") int count); + + @Headers("Authentication-Required: true") + @POST("reading/{slug}/{state}/") + Single setReadingState(@Path("slug") String slug, @Path("state") String state); + + @Headers("Authentication-Required: true") + @GET("reading/{slug}/") + Single getReadingState(@Path("slug") String slug); + + @Headers({"Authentication-Required: true", "New-Api: true"}) + @GET("shelf/{state}/") + Call> getReadenBooks(@Path("state") String state, @Query("after") String lastKey, @Query("count") int count); + + @Headers("Authentication-Required: true") + @POST("like/{slug}/") + Single setFavouriteState(@Path("slug") String slug, @Query("action") String action); + + @Headers("Authentication-Required: true") + @GET("like/{slug}/") + Single getFavouriteState(@Path("slug") String slug); + + @GET("preview/") + Call> getPreview(); + + @GET("books/{slug}") + Call getPreviewMockup(@Path("slug") String slug); + + @Headers({"Authentication-Required: true", "New-Api: true"}) + @GET("shelf/likes/") + Call> getFavourites(@Query("after") String lastKey, @Query("count") int count); + +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/CategoriesService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/CategoriesService.java new file mode 100644 index 0000000..c51b088 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/CategoriesService.java @@ -0,0 +1,25 @@ +package com.moiseum.wolnelektury.connection.services; + +import com.moiseum.wolnelektury.connection.models.CategoryModel; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +/** + * Created by piotrostrowski on 25.11.2017. + */ + +public interface CategoriesService { + + @GET("epochs") + Call> getEpochs(@Query("book_only") boolean bookOnly); + + @GET("genres") + Call> getGenres(@Query("book_only") boolean bookOnly); + + @GET("kinds") + Call> getKinds(@Query("book_only") boolean bookOnly); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/NewsService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/NewsService.java new file mode 100644 index 0000000..fa317eb --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/NewsService.java @@ -0,0 +1,16 @@ +package com.moiseum.wolnelektury.connection.services; + +import com.moiseum.wolnelektury.connection.models.NewsModel; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface NewsService { + + @GET("blog") + Call> getNews(@Query("after") String lastKey, @Query("count") int count); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/UserService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/UserService.java new file mode 100644 index 0000000..9ce35a9 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/UserService.java @@ -0,0 +1,26 @@ +package com.moiseum.wolnelektury.connection.services; + +import com.moiseum.wolnelektury.connection.models.OAuthTokenModel; +import com.moiseum.wolnelektury.connection.models.UserModel; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Headers; + +/** + * Created by Piotr Ostrowski on 06.06.2018. + */ +public interface UserService { + + @Headers("Token-Requested: true") + @GET("oauth/request_token/") + Call requestToken(); + + @Headers("Token-Requested: true") + @GET("oauth/access_token/") + Call accessToken(); + + @Headers("Authentication-Required: true") + @GET("username/") + Call getUser(); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/events/BookFavouriteEvent.java b/Android/app/src/main/java/com/moiseum/wolnelektury/events/BookFavouriteEvent.java new file mode 100644 index 0000000..0bd4497 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/events/BookFavouriteEvent.java @@ -0,0 +1,13 @@ +package com.moiseum.wolnelektury.events; + +public class BookFavouriteEvent { + private boolean state; + + public BookFavouriteEvent(boolean state) { + this.state = state; + } + + public boolean getState() { + return state; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/events/LoggedInEvent.java b/Android/app/src/main/java/com/moiseum/wolnelektury/events/LoggedInEvent.java new file mode 100644 index 0000000..df2cbd7 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/events/LoggedInEvent.java @@ -0,0 +1,7 @@ +package com.moiseum.wolnelektury.events; + +/** + * Created by Piotr Ostrowski on 11.09.2018. + */ +public class LoggedInEvent { +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/storage/BookStorage.java b/Android/app/src/main/java/com/moiseum/wolnelektury/storage/BookStorage.java new file mode 100644 index 0000000..31d6628 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/storage/BookStorage.java @@ -0,0 +1,91 @@ +package com.moiseum.wolnelektury.storage; + +import android.app.Application; +import android.support.annotation.Nullable; + +import com.moiseum.wolnelektury.BuildConfig; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.BookModel_; +import com.moiseum.wolnelektury.connection.models.MyObjectBox; + +import org.greenrobot.eventbus.EventBus; + +import java.util.List; + +import io.objectbox.Box; +import io.objectbox.BoxStore; +import io.objectbox.android.AndroidObjectBrowser; +import io.objectbox.query.Query; + +/** + * @author golonkos + */ + +public class BookStorage { + + + public static class BookAddedEvent { + } + + public static class BookDeletedEvent { + private final String slug; + + public BookDeletedEvent(String slug) { + this.slug = slug; + } + + public String getSlug() { + return slug; + } + } + + private BoxStore boxStore; + + public BookStorage(Application application) { + boxStore = MyObjectBox.builder().androidContext(application).build(); + if (BuildConfig.DEBUG) { + new AndroidObjectBrowser(boxStore).start(application); + } + } + + private Box getBox() { + return boxStore.boxFor(BookModel.class); + } + + public void add(BookModel book) { + getBox().put(book); + EventBus.getDefault().post(new BookAddedEvent()); + } + + public void update(BookModel book) { + getBox().put(book); + } + + @Nullable + public BookModel find(String slug) { + Query query = getBox().query().equal(BookModel_.slug, slug).build(); + return query.findFirst(); + } + + public boolean exists(String slug) { + return find(slug) != null; + } + + public void remove(String slug, boolean notify) { + BookModel book = find(slug); + if (book != null) { + getBox().remove(book); + if (notify) { + EventBus.getDefault().post(new BookDeletedEvent(slug)); + } + } + } + + public List all() { + return getBox().getAll(); + } + + public void removeAll() { + getBox().removeAll(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/storage/StringListConverter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/storage/StringListConverter.java new file mode 100644 index 0000000..24b45b7 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/storage/StringListConverter.java @@ -0,0 +1,37 @@ +package com.moiseum.wolnelektury.storage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.objectbox.converter.PropertyConverter; + +/** + * Created by Piotr Ostrowski on 01.07.2018. + */ +public class StringListConverter implements PropertyConverter, String> { + + @Override + public List convertToEntityProperty(String databaseValue) { + if (databaseValue == null) { + return new ArrayList<>(); + } + return Arrays.asList(databaseValue.split(",")); + } + + @Override + public String convertToDatabaseValue(List entityProperty) { + if (entityProperty == null) { + return ""; + } + if (entityProperty.isEmpty()) { + return ""; + } + StringBuilder builder = new StringBuilder(); + for (String property : entityProperty) { + builder.append(property).append(","); + } + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/utils/SharedPreferencesUtils.java b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/SharedPreferencesUtils.java new file mode 100644 index 0000000..6120e64 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/SharedPreferencesUtils.java @@ -0,0 +1,123 @@ +package com.moiseum.wolnelektury.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.moiseum.wolnelektury.connection.models.OAuthTokenModel; + +import de.adorsys.android.securestoragelibrary.SecurePreferences; + +/** + * Created by Piotr Ostrowski on 14.06.2018. + */ +public final class SharedPreferencesUtils { + + private static final String PREFERENCES_FILENAME = "WolneLekturyPreferences"; + private static final String ACCESS_TOKEN_KEY = "AccessToken"; + private static final String ACCESS_TOKEN_SECRET_KEY = "AccessTokenSecret"; + private static final String USERNAME_KEY = "Username"; + private static final String PREMIUM_KEY = "Premium"; + private static final String NOTIFICATIONS_KEY = "Notifications"; + private static final String TEMPORARY_LOGIN_TOKEN_KEY = "TemporaryLoginTokenKey"; + + private OAuthTokenModel currentToken; + private String username; + private Boolean isPremium; + private Boolean notifications; + private String temporaryLoginToken; + private SharedPreferences preferences; + + public SharedPreferencesUtils(Context context) { + this.preferences = context.getSharedPreferences(PREFERENCES_FILENAME, Context.MODE_PRIVATE); + } + + public void storeAccessToken(OAuthTokenModel tokenModel) { + currentToken = tokenModel; + SecurePreferences.setValue(ACCESS_TOKEN_KEY, tokenModel.getToken()); + SecurePreferences.setValue(ACCESS_TOKEN_SECRET_KEY, tokenModel.getTokenSecret()); + } + + public OAuthTokenModel getAccessToken() { + if (currentToken != null) { + return currentToken; + } + + String token = SecurePreferences.getStringValue(ACCESS_TOKEN_KEY, null); + String tokenSecret = SecurePreferences.getStringValue(ACCESS_TOKEN_SECRET_KEY, null); + + if (token == null || tokenSecret == null) { + return null; + } + currentToken = new OAuthTokenModel(); + currentToken.setToken(token); + currentToken.setTokenSecret(tokenSecret); + return currentToken; + } + + public String getUsername() { + if (username == null) { + username = preferences.getString(USERNAME_KEY, null); + } + return username; + } + + public void setUsername(String username) { + this.username = username; + preferences.edit().putString(USERNAME_KEY, username).apply(); + } + + public boolean isUserLoggedIn() { + return getAccessToken() != null; + } + + public boolean isUserPremium() { + if (isPremium == null) { + isPremium = preferences.getBoolean(PREMIUM_KEY, false); + } + return isPremium && isUserLoggedIn(); + } + + public void setPremium(boolean isPremium) { + this.isPremium = isPremium; + preferences.edit().putBoolean(PREMIUM_KEY, isPremium).apply(); + } + + public boolean getNotifications() { + if (notifications == null) { + notifications = preferences.getBoolean(NOTIFICATIONS_KEY, true); + } + return notifications; + } + + public void setNotifications(Boolean notifications) { + this.notifications = notifications; + preferences.edit().putBoolean(NOTIFICATIONS_KEY, notifications).apply(); + } + + public String getTemporaryLoginToken() { + if (temporaryLoginToken == null) { + temporaryLoginToken = preferences.getString(TEMPORARY_LOGIN_TOKEN_KEY, null); + } + return temporaryLoginToken; + } + + public void setTemporaryLoginToken(String temporaryLoginToken) { + this.temporaryLoginToken = temporaryLoginToken; + preferences.edit().putString(TEMPORARY_LOGIN_TOKEN_KEY, temporaryLoginToken).apply(); + } + + public void clearUserData() { + currentToken = null; + username = null; + isPremium = null; + notifications = null; + temporaryLoginToken = null; + SecurePreferences.clearAllValues(); + preferences.edit() + .remove(USERNAME_KEY) + .remove(PREMIUM_KEY) + .remove(NOTIFICATIONS_KEY) + .remove(TEMPORARY_LOGIN_TOKEN_KEY) + .apply(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/utils/StringUtils.java b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/StringUtils.java new file mode 100644 index 0000000..d890e9a --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/StringUtils.java @@ -0,0 +1,42 @@ +package com.moiseum.wolnelektury.utils; + +import com.moiseum.wolnelektury.connection.models.CategoryModel; + +import java.util.List; + +/** + * Created by piotrostrowski on 19.11.2017. + */ + +public class StringUtils { + + public static String joinCategory(List list, String delim) { + StringBuilder sb = new StringBuilder(); + String loopDelimiter = ""; + + for (CategoryModel s : list) { + sb.append(loopDelimiter); + sb.append(s.getName()); + loopDelimiter = delim; + } + + return sb.toString(); + } + + public static String joinSlugs(List list, String delim) { + if (list.size() == 0) { + return null; + } + + StringBuilder sb = new StringBuilder(); + String loopDelimiter = ""; + + for (CategoryModel s : list) { + sb.append(loopDelimiter); + sb.append(s.getSlug()); + loopDelimiter = delim; + } + + return sb.toString(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/utils/TrackerUtils.java b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/TrackerUtils.java new file mode 100644 index 0000000..40d694c --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/utils/TrackerUtils.java @@ -0,0 +1,29 @@ +package com.moiseum.wolnelektury.utils; + +import android.content.Context; + +import com.moiseum.wolnelektury.base.WLApplication; + +import org.piwik.sdk.Piwik; +import org.piwik.sdk.Tracker; +import org.piwik.sdk.TrackerConfig; +import org.piwik.sdk.extra.TrackHelper; + +/** + * @author golonkos + */ + +public final class TrackerUtils { + + private static final String PIWIK_URL = "https://piwik.nowoczesnapolska.org.pl/nocas/piwik.php"; + private static final String TRACKER_NAME = "MainTracker"; + + public static Tracker create(Context context) { + return Piwik.getInstance(context).newTracker(new TrackerConfig(PIWIK_URL, 29, TRACKER_NAME)); + } + + public static void trackScreen(String path, String name) { + Tracker tracker = WLApplication.getInstance().getTracker(); + TrackHelper.track().screen(path).title(name).with(tracker); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/AboutFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/AboutFragment.java new file mode 100644 index 0000000..e6c1144 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/AboutFragment.java @@ -0,0 +1,75 @@ +package com.moiseum.wolnelektury.view; + +import android.os.Bundle; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractFragment; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.events.LoggedInEvent; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import butterknife.BindView; +import butterknife.OnClick; + +/** + * @author golonkos + */ + +public class AboutFragment extends AbstractFragment { + + public static AboutFragment newInstance() { + return new AboutFragment(); + } + + @BindView(R.id.btnBecomeAFriend) + Button btnBecomeAFriend; + @BindView(R.id.tvAbout) + TextView tvAbout; + + private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_about; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + btnBecomeAFriend.setVisibility(preferences.isUserPremium() ? View.GONE : View.VISIBLE); + tvAbout.setText(Html.fromHtml(getString(R.string.about_text))); + tvAbout.setLinksClickable(true); + tvAbout.setMovementMethod(LinkMovementMethod.getInstance()); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EventBus.getDefault().register(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + EventBus.getDefault().unregister(this); + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onLoggedIn(LoggedInEvent event) { + btnBecomeAFriend.setVisibility(preferences.isUserPremium() ? View.GONE : View.VISIBLE); + } + + @OnClick(R.id.btnBecomeAFriend) + public void onBecomeAFriendClicked() { + showPayPalForm(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewActivity.java new file mode 100644 index 0000000..1e30d1f --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewActivity.java @@ -0,0 +1,42 @@ +package com.moiseum.wolnelektury.view; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.base.AbstractIntent; + +/** + * @author golonkos + */ + +public class WebViewActivity extends AbstractActivity { + + private static final String PARAM_URL = "PARAM_URL"; + + public static class WebViewIntent extends AbstractIntent { + + public WebViewIntent(Context packageContext, String url) { + super(packageContext, WebViewActivity.class); + putExtra(PARAM_URL, url); + } + } + + @Override + public int getLayoutResourceId() { + return R.layout.activity_blank; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + setBackButtonEnable(true); + + if (savedInstanceState == null) { + String url = getIntent().getStringExtra(PARAM_URL); + Fragment fragment = WebViewFragment.newInstance(url); + getSupportFragmentManager().beginTransaction().add(R.id.flContainer, fragment).commit(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewFragment.java new file mode 100644 index 0000000..38bc5dc --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewFragment.java @@ -0,0 +1,115 @@ +package com.moiseum.wolnelektury.view; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.View; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Button; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractFragment; + +import butterknife.BindView; + +/** + * @author golonkos. + */ + +public class WebViewFragment extends AbstractFragment { + + private static final String PARAM_URL = "PARAM_URL"; + + @BindView(R.id.wvAbout) + WebView wvPage; + @BindView(R.id.btnBack) + Button btnBack; + @BindView(R.id.btnRefresh) + Button btnRefresh; + @BindView(R.id.btnNext) + Button btnNext; + @BindView(R.id.tvPageError) + TextView tvPageError; + + private boolean loadFailed; + + public static Fragment newInstance(String url) { + WebViewFragment fragment = new WebViewFragment(); + Bundle args = new Bundle(1); + args.putString(PARAM_URL, url); + fragment.setArguments(args); + return fragment; + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_web_view; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + initWebView(); + } + + private void initWebView() { + wvPage.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); + wvPage.getSettings().setJavaScriptEnabled(true); + wvPage.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); + wvPage.setWebViewClient(new WebViewClient() { + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + wvPage.loadUrl(url); + return true; + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + if (tvPageError == null) { + return; + } + loadFailed = true; + tvPageError.setVisibility(View.VISIBLE); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + if (tvPageError == null) { + return; + } + if (!loadFailed) { + tvPageError.setVisibility(View.GONE); + } + btnBack.setEnabled(wvPage.canGoBack()); + btnNext.setEnabled(wvPage.canGoForward()); + } + }); + String url = getArguments().getString(PARAM_URL); + wvPage.loadUrl(url); + + btnRefresh.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + loadFailed = false; + wvPage.reload(); + } + }); + + btnBack.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + wvPage.goBack(); + } + }); + + btnNext.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + wvPage.goForward(); + } + }); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookActivity.java new file mode 100644 index 0000000..8f542af --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookActivity.java @@ -0,0 +1,53 @@ +package com.moiseum.wolnelektury.view.book; + +import android.content.Context; +import android.os.Bundle; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.base.AbstractIntent; + +/** + * Created by Piotr Ostrowski on 17.11.2017. + */ + +public class BookActivity extends AbstractActivity { + + private static final String BOOK_FRAGMENT_TAG = "BookFragmentTag"; + static final String BOOK_SLUG_KEY = "BookSlugKey"; + static final String BOOK_TYPE_KEY = "BookTypeKey"; + + public static class BookIntent extends AbstractIntent { + + public BookIntent(String slug, BookType type, Context context) { + super(context, BookActivity.class); + putExtra(BOOK_SLUG_KEY, slug); + putExtra(BOOK_TYPE_KEY, type.name()); + } + } + + @Override + public int getLayoutResourceId() { + return R.layout.activity_blank; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + setTitle(""); + if (!getIntent().hasExtra(BOOK_SLUG_KEY)) { + throw new IllegalStateException("Missing either slug or full ebook model."); + } + if (!getIntent().hasExtra(BOOK_TYPE_KEY)) { + throw new IllegalStateException("Missing book type."); + } + + String bookSlug = getIntent().getStringExtra(BOOK_SLUG_KEY); + BookType type = BookType.valueOf(getIntent().getStringExtra(BOOK_TYPE_KEY)); + + BookFragment bookFragment = (BookFragment) getSupportFragmentManager().findFragmentByTag(BOOK_FRAGMENT_TAG); + if (bookFragment == null) { + bookFragment = BookFragment.newInstance(bookSlug, type); + getSupportFragmentManager().beginTransaction().add(R.id.flContainer, bookFragment, BOOK_FRAGMENT_TAG).commit(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookFragment.java new file mode 100644 index 0000000..66282c3 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookFragment.java @@ -0,0 +1,358 @@ +package com.moiseum.wolnelektury.view.book; + +import android.content.Intent; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.os.Bundle; +import android.support.constraint.ConstraintLayout; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.CollapsingToolbarLayout; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.facebook.shimmer.ShimmerFrameLayout; +import com.folioreader.Config; +import com.folioreader.util.FolioReader; +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.utils.StringUtils; +import com.moiseum.wolnelektury.view.book.components.ProgressDownloadButton; +import com.moiseum.wolnelektury.view.player.PlayerActivity; + +import org.sufficientlysecure.htmltextview.HtmlTextView; + +import butterknife.BindView; +import butterknife.OnClick; + +import static android.app.Activity.RESULT_OK; +import static com.folioreader.ui.folio.activity.FolioActivity.PARAM_CHAPTERS_COUNT; +import static com.folioreader.ui.folio.activity.FolioActivity.PARAM_CURRENT_CHAPTER; +import static com.folioreader.ui.folio.activity.FolioActivity.PARAM_FILE_NAME; +import static com.moiseum.wolnelektury.view.book.BookActivity.BOOK_SLUG_KEY; +import static com.moiseum.wolnelektury.view.book.BookActivity.BOOK_TYPE_KEY; + +/** + * Created by Piotr Ostrowski on 17.11.2017. + */ + +public class BookFragment extends PresenterFragment implements BookView { + + private static final int BOOK_READER_CODE = 32434; + private static final String DEFAULT_OVERLAY_COLOR = "#80db4b16"; + + public static BookFragment newInstance(String slug, BookType type) { + BookFragment bookFragment = new BookFragment(); + Bundle args = new Bundle(); + args.putString(BOOK_SLUG_KEY, slug); + args.putString(BOOK_TYPE_KEY, type.name()); + bookFragment.setArguments(args); + return bookFragment; + } + + @BindView(R.id.clMainView) + View clMainView; + @BindView(R.id.ctlCollapse) + CollapsingToolbarLayout ctlCollapse; + @BindView(R.id.ivCoverBackground) + ImageView ivCoverBackground; + @BindView(R.id.ivCover) + ImageView ivCover; + @BindView(R.id.vCoverOverlay) + View vCoverOverlay; + @BindView(R.id.tvBookAuthor) + TextView tvBookAuthor; + @BindView(R.id.tvBookTitle) + TextView tvBookTitle; + @BindView(R.id.btnEbook) + ProgressDownloadButton btnEbook; + @BindView(R.id.btnAudiobook) + ProgressDownloadButton btnAudiobook; + @BindView(R.id.tvBookKind) + TextView tvBookKind; + @BindView(R.id.tvBookGenre) + TextView tvBookGenre; + @BindView(R.id.tvBookEpoch) + TextView tvBookEpoch; + @BindView(R.id.tvQuotationText) + HtmlTextView tvQuotationText; + @BindView(R.id.tvQuotationAuthor) + TextView tvQuotationAuthor; + @BindView(R.id.ibDeleteEbook) + ImageButton ibDeleteEbook; + @BindView(R.id.ibDeleteAudiobook) + ImageButton ibDeleteAudiobook; + @BindView(R.id.pbHeaderLoading) + ProgressBar pbHeaderLoading; + @BindView(R.id.rlHeaderLoadingContainer) + RelativeLayout rlHeaderLoadingContainer; + @BindView(R.id.fabShare) + FloatingActionButton fabShare; + @BindView(R.id.fabFavourite) + FloatingActionButton fabFavourite; + @BindView(R.id.shimmerContentContainer) + ShimmerFrameLayout shimmerContentContainer; + @BindView(R.id.rlEbookButtonsContainer) + RelativeLayout rlEbookButtonsContainer; + @BindView(R.id.rlAudioButtonsContainer) + RelativeLayout rlAudioButtonsContainer; + @BindView(R.id.vSecondDivider) + View vSecondDivider; + @BindView(R.id.ibRetry) + ImageButton ibRetry; + @BindView(R.id.clPremium) + View clPremium; + + @Override + protected BookPresenter createPresenter() { + if (getArguments() == null || getArguments().getString(BOOK_SLUG_KEY) == null || getArguments().getString(BOOK_TYPE_KEY) == null) { + throw new IllegalStateException("Missing BookDetails data!"); + } + BookType type = BookType.valueOf(getArguments().getString(BOOK_TYPE_KEY)); + return new BookPresenter(getArguments().getString(BOOK_SLUG_KEY), type, this); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_book; + } + + @SuppressWarnings("ConstantConditions") + @Override + public void prepareView(View view, Bundle savedInstanceState) { + shimmerContentContainer.startShimmerAnimation(); + Toolbar toolbar = view.findViewById(R.id.bookToolbar); + setupToolbar(toolbar); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == BOOK_READER_CODE && resultCode == RESULT_OK) { + String bookName = data.getStringExtra(PARAM_FILE_NAME); + int currentChapter = data.getIntExtra(PARAM_CURRENT_CHAPTER, 0); + int count = data.getIntExtra(PARAM_CHAPTERS_COUNT, 0); + getPresenter().onBackFromReader(bookName, currentChapter, count); + } + } + + @OnClick(R.id.btnEbook) + public void onEbookClick() { + getPresenter().launchEbookForState(btnEbook.getState()); + } + + @OnClick(R.id.btnAudiobook) + public void onAudiobookClick() { + getPresenter().launchAudiobookForState(btnAudiobook.getState()); + } + + @OnClick(R.id.ibDeleteEbook) + public void onDeleteEbookClick() { + getPresenter().deleteEbook(); + Toast.makeText(getContext() ,getString(R.string.book_deleted_message), Toast.LENGTH_SHORT).show(); + } + + @OnClick(R.id.ibDeleteAudiobook) + public void onDeleteAudiobookClick() { + getPresenter().deleteAudiobook(); + Toast.makeText(getContext() ,getString(R.string.book_deleted_message), Toast.LENGTH_SHORT).show(); + } + + @OnClick(R.id.ibRetry) + public void onRetryClick() { + ibRetry.setVisibility(View.GONE); + pbHeaderLoading.setVisibility(View.VISIBLE); + getPresenter().reloadBookDetails(); + } + + @OnClick(R.id.fabShare) + public void onShareClick() { + getPresenter().onShareEbookClicked(); + } + + @OnClick(R.id.fabFavourite) + public void onFavouriteClick() { + getPresenter().onFavouriteEbookClicked(); + } + + @OnClick(R.id.bSupportUs) + public void onSupportUsClick() { + showPayPalForm(); + } + + // ------------------------------------------------------------------------------------------------------------------------------------------ + // BookView + // ------------------------------------------------------------------------------------------------------------------------------------------ + + @Override + public void initializeBookView(BookDetailsModel book) { + shimmerContentContainer.stopShimmerAnimation(); + fabShare.setVisibility(View.VISIBLE); + getPresenter().showFavouriteButton(book); + rlHeaderLoadingContainer.setVisibility(View.GONE); + rlEbookButtonsContainer.setVisibility(View.VISIBLE); + + if (book.hasAudio()) { + rlAudioButtonsContainer.setVisibility(View.VISIBLE); + } + ctlCollapse.setTitle(book.getTitle()); + ctlCollapse.setExpandedTitleColor(getResources().getColor(android.R.color.transparent)); + + Glide.with(getContext()).load(book.getCover()).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivCoverBackground); + Glide.with(getContext()).load(book.getCover()).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivCover); + + vCoverOverlay.setAlpha(0.7f); + String colorHash = book.getCoverColor() != null ? book.getCoverColor() : DEFAULT_OVERLAY_COLOR; + vCoverOverlay.setBackgroundColor(Color.parseColor(colorHash)); + + if (book.getAuthors() != null && book.getAuthors().size() > 0) { + tvBookAuthor.setText(StringUtils.joinCategory(book.getAuthors(), ", ")); + } + tvBookTitle.setText(book.getTitle()); + + if (book.getKinds() != null && book.getKinds().size() > 0) { + tvBookKind.setText(StringUtils.joinCategory(book.getKinds(), ", ")); + } + tvBookKind.setBackgroundColor(Color.TRANSPARENT); + if (book.getGenres() != null && book.getGenres().size() > 0) { + tvBookGenre.setText(StringUtils.joinCategory(book.getGenres(), ", ")); + } + tvBookGenre.setBackgroundColor(Color.TRANSPARENT); + if (book.getEpochs() != null && book.getEpochs().size() > 0) { + tvBookEpoch.setText(StringUtils.joinCategory(book.getEpochs(), ", ")); + } + tvBookEpoch.setBackgroundColor(Color.TRANSPARENT); + + if (book.getFragment() != null) { + tvQuotationText.setMinLines(0); + tvQuotationText.setHtml(book.getFragment().getHtml()); + tvQuotationText.setBackgroundColor(Color.TRANSPARENT); + tvQuotationAuthor.setText(book.getFragment().getTitle()); + tvQuotationAuthor.setBackgroundColor(Color.TRANSPARENT); + } else { + tvQuotationText.setVisibility(View.GONE); + tvQuotationAuthor.setVisibility(View.GONE); + vSecondDivider.setVisibility(View.GONE); + } + enableToolbarCollapse(); + } + + private void enableToolbarCollapse() { + ViewTreeObserver viewTreeObserver = shimmerContentContainer.getViewTreeObserver(); + if (viewTreeObserver.isAlive()) { + viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + shimmerContentContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); + if (ctlCollapse.getHeight() + shimmerContentContainer.getHeight() > clMainView.getHeight()) { + AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) ctlCollapse.getLayoutParams(); + params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED); // list other flags here by | + ctlCollapse.setLayoutParams(params); + } + } + }); + } + } + + @Override + public void changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState state, boolean forAudiobook) { + if (forAudiobook) { + ibDeleteAudiobook.setVisibility(state.isDeletable() ? View.VISIBLE : View.INVISIBLE); + btnAudiobook.setState(state); + } else { + ibDeleteEbook.setVisibility(state.isDeletable() ? View.VISIBLE : View.INVISIBLE); + btnEbook.setState(state); + } + } + + @Override + public void showCurrentStateProgress(int percentage, boolean forAudiobook) { + if (forAudiobook) { + btnAudiobook.setProgress(percentage); + } else { + btnEbook.setProgress(percentage); + } + } + + @Override + public void showInitializationError() { + Toast.makeText(getContext(), R.string.book_loading_error, Toast.LENGTH_LONG).show(); + pbHeaderLoading.setVisibility(View.GONE); + ibRetry.setVisibility(View.VISIBLE); + } + + @Override + public void showDownloadFileError() { + Toast.makeText(getContext(), R.string.book_download_error, Toast.LENGTH_LONG).show(); + } + + @Override + public void startShareActivity(String shareUrl) { + showShareActivity(shareUrl); + } + + @Override + public void openBook(String downloadedBookUrl) { + FolioReader reader = new FolioReader(getContext()); + Config config = new Config.ConfigBuilder().build(); + Intent bookIntent = reader.createBookIntent(downloadedBookUrl, config); + startActivityForResult(bookIntent, BOOK_READER_CODE); + } + + @Override + public void launchPlayer(BookDetailsModel book) { + if (getArguments() != null) { + PlayerActivity.PlayerIntent intent = new PlayerActivity.PlayerIntent(book, getArguments().getString(BOOK_SLUG_KEY), getContext()); + startActivity(intent); + } + } + + @Override + public void updateReadingProgress(int currentChapter, int count, boolean forAudiobook) { + if (forAudiobook) { + btnAudiobook.setCurrentReadCount(currentChapter + 1, count); + } else { + btnEbook.setCurrentReadCount(currentChapter, count); + } + } + + @Override + public void startLikeClicked() { + fabFavourite.setImageResource(R.drawable.ic_fav_active); + } + + @Override + public void stopLikeClicked() { + fabFavourite.setImageResource(R.drawable.ic_fav); + } + + public void showFavouriteButton(BookDetailsModel book) { + fabFavourite.setVisibility(View.VISIBLE); + if(book.getFavouriteState()) { + fabFavourite.setImageResource(R.drawable.ic_fav_active); + } + } + + @Override + public void showPremiumLock(boolean lock) { + if (!lock) { + clPremium.setVisibility(View.GONE); + } else { + clPremium.setVisibility(View.VISIBLE); + btnEbook.setClickable(false); + btnAudiobook.setClickable(false); + fabFavourite.setClickable(false); + fabShare.setClickable(false); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookPresenter.java new file mode 100644 index 0000000..726e7eb --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookPresenter.java @@ -0,0 +1,321 @@ +package com.moiseum.wolnelektury.view.book; + +import android.os.Bundle; +import android.util.Log; + +import com.folioreader.util.AppUtil; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.downloads.FileCacheUtils; +import com.moiseum.wolnelektury.connection.downloads.FileDownloadIntentService; +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.FavouriteStateModel; +import com.moiseum.wolnelektury.connection.models.ReadingStateModel; +import com.moiseum.wolnelektury.connection.services.BooksService; +import com.moiseum.wolnelektury.events.BookFavouriteEvent; +import com.moiseum.wolnelektury.storage.BookStorage; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; +import com.moiseum.wolnelektury.view.book.components.ProgressDownloadButton; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +/** + * Created by Piotr Ostrowski on 17.11.2017. + */ + +class BookPresenter extends FragmentPresenter { + + private static final String TAG = BookPresenter.class.getSimpleName(); + + private BookDetailsModel book; + private BookModel storedBook; + private String bookSlug; + private BookType bookType; + + private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + private BooksService booksService = WLApplication.getInstance().getRestClient().obtainBookService(); + private BookStorage storage = WLApplication.getInstance().getBookStorage(); + + BookPresenter(String slug, BookType type, BookView view) { + super(view); + this.bookSlug = slug; + this.bookType = type; + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + loadBookDetails(); + getView().showPremiumLock(!preferences.isUserPremium() && bookType.shouldShowPremiumLock()); + EventBus.getDefault().register(this); + } + + @Override + public void onResume() { + super.onResume(); + this.storedBook = storage.find(bookSlug); + if (storedBook != null && storedBook.isEbookDownloaded()) { + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_READING, false); + getView().updateReadingProgress(storedBook.getCurrentChapter(), storedBook.getTotalChapters(), false); + } + if (storedBook != null && storedBook.isAudioDownloaded()) { + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_READING, true); + getView().updateReadingProgress(storedBook.getCurrentAudioChapter(), storedBook.getTotalAudioChapters(), true); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onMessageEvent(FileCacheUtils.DownloadProgressEvent event) { + if (event.getFileUrl().equals(book.getEpub())) { + int percentage = (int) ((double) event.getDownloaded() / (double) event.getTotal() * 100.0); + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, false); + getView().showCurrentStateProgress(percentage, false); + } else if (book.getAudiobookFilesUrls().contains(event.getFileUrl())) { + ArrayList filesUrls = book.getAudiobookFilesUrls(); + int fileIndex = filesUrls.indexOf(event.getFileUrl()); + double part = (double) event.getDownloaded() / (double) event.getTotal() / (double) filesUrls.size(); + double completed = (double) fileIndex / (double) filesUrls.size(); + int percentage = (int) ((part + completed) * 100.0); + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, true); + getView().showCurrentStateProgress(percentage, true); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onMessageEvent(FileDownloadIntentService.DownloadFileEvent event) { + if (event.getFileUrl().equals(book.getEpub())) { + if (event.isSuccess()) { + storeDownloadedBook(false); + getView().showCurrentStateProgress(0, false); + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_READING, false); + launchFolioReader(); + } else { + getView().showCurrentStateProgress(0, false); + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_INITIAL, false); + getView().showDownloadFileError(); + } + } else if (book.getAudiobookFilesUrls().contains(event.getFileUrl())) { + if (event.isSuccess()) { + ArrayList filesUrls = book.getAudiobookFilesUrls(); + int fileIndex = filesUrls.indexOf(event.getFileUrl()); + if (fileIndex == filesUrls.size() - 1) { + storeDownloadedBook(true); + getView().showCurrentStateProgress(0, true); + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_READING, true); + launchAudioPlayer(); + } else { + int percentage = (int) ((fileIndex + 1.0) / ((double) filesUrls.size()) * 100.0); + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, true); + getView().showCurrentStateProgress(percentage, true); + } + } else { + getView().showCurrentStateProgress(0, true); + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_INITIAL, true); + getView().showDownloadFileError(); + } + + } + } + + void launchEbookForState(ProgressDownloadButton.ProgressDownloadButtonState state) { + if (state.isDownloaded()) { + launchFolioReader(); + } else { + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, false); + FileDownloadIntentService.downloadFile(getView().getContext(), book.getEpub()); + } + } + + void launchAudiobookForState(ProgressDownloadButton.ProgressDownloadButtonState state) { + if (state.isDownloaded()) { + launchAudioPlayer(); + } else { + getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_DOWNLOADING, true); + FileDownloadIntentService.downloadFiles(getView().getContext(), book.getAudiobookFilesUrls()); + } + } + + void reloadBookDetails() { + loadBookDetails(); + } + + void deleteEbook() { + addDisposable(FileCacheUtils.deleteEbookFile(book.getEpub()) + .andThen(Completable.fromAction(this::updateStoredBookAfterDeletion)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_INITIAL, false), + error -> { } + ) + ); + } + + void deleteAudiobook() { + addDisposable(FileCacheUtils.deleteAudiobookFiles(book.getAudiobookFilesUrls()) + .andThen(Completable.fromAction(this::updateStoredAudiobookAfterDeletion)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> getView().changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState.STATE_INITIAL, true), + error -> { } + ) + ); + } + + void onShareEbookClicked() { + getView().startShareActivity(book.getUrl()); + } + + void onFavouriteEbookClicked() { + if (!book.getFavouriteState()) { + getView().startLikeClicked(); + updateFavouriteState(true); + } else { + getView().stopLikeClicked(); + updateFavouriteState(false); + } + } + + void onBackFromReader(String bookName, int currentChapter, int count) { + storedBook.setEbookName(bookName); + storedBook.setCurrentChapter(currentChapter); + storedBook.setTotalChapters(count); + storage.update(storedBook); + getView().updateReadingProgress(currentChapter, count, false); + + if (currentChapter == count && book.getState() == ReadingStateModel.ReadingState.STATE_READING) { + updateReadingState(ReadingStateModel.ReadingState.STATE_COMPLETED); + } + } + + // ------------------------------------------------------------------------------------------------------------------------------------------ + // Helper methods + // ------------------------------------------------------------------------------------------------------------------------------------------ + + private void updateStoredBookAfterDeletion() { + if (!storedBook.isAudioDownloaded()) { + storage.remove(storedBook.getSlug(), true); + storedBook = null; + return; + } + + AppUtil.removeBookState(WLApplication.getInstance().getApplicationContext(), storedBook.getEbookName()); + storedBook.setEbookName(null); + storedBook.setEbookFileUrl(null); + storedBook.setCurrentChapter(0); + storedBook.setTotalChapters(0); + storage.update(storedBook); + } + + private void updateStoredAudiobookAfterDeletion() { + if (!storedBook.isEbookDownloaded()) { + storage.remove(storedBook.getSlug(), true); + storedBook = null; + return; + } + + storedBook.setAudioFileUrls(null); + storedBook.setCurrentAudioChapter(0); + storedBook.setTotalAudioChapters(0); + storage.update(storedBook); + } + + private Single getBookDetails() { + Single readingStateSingle = preferences.isUserLoggedIn() ? booksService.getReadingState(bookSlug) : Single.just(new ReadingStateModel()); + Single favouriteStateModelSingle = preferences.isUserLoggedIn() ? booksService.getFavouriteState(bookSlug) : Single.just(new FavouriteStateModel()); + Single bookDetailsSingle = Single.zip( + readingStateSingle, + favouriteStateModelSingle, + booksService.getBookDetails(bookSlug), + (readingStateModel, favouriteStateModel, bookDetailsModel) -> { + bookDetailsModel.setState(readingStateModel.getState()); + bookDetailsModel.setFavouriteState(favouriteStateModel.getState()); + return bookDetailsModel; + }); + return bookDetailsSingle.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); + } + + private void loadBookDetails() { + addDisposable(getBookDetails().subscribe(bookDetailsModel -> { + book = bookDetailsModel; + getView().initializeBookView(bookDetailsModel); + }, error -> getView().showInitializationError())); + } + + private void updateReadingState(ReadingStateModel.ReadingState state) { + if (preferences.isUserLoggedIn()) { + addDisposable(booksService.setReadingState(bookSlug, state.getStateName()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(updatedState -> book.setState(updatedState.getState()), error -> Log.e(TAG, "Failed to update reading state.", error))); + } + } + + private void updateFavouriteState(boolean state) { + addDisposable(booksService.setFavouriteState(bookSlug, book.getFavouriteString(state)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(updatedState -> { + book.setFavouriteState(state); + EventBus.getDefault().post(new BookFavouriteEvent(state)); + }, error -> Log.e(TAG, "Failed to update favourite state.", error))); + } + + private void launchFolioReader() { + if (book.getState() == ReadingStateModel.ReadingState.STATE_NOT_STARTED) { + updateReadingState(ReadingStateModel.ReadingState.STATE_READING); + } + String downloadedBookUrl = FileCacheUtils.getCachedFileForUrl(book.getEpub()); + if (downloadedBookUrl != null) { + getView().openBook(downloadedBookUrl); + } + } + + private void launchAudioPlayer() { + if (book.getState() == ReadingStateModel.ReadingState.STATE_NOT_STARTED) { + updateReadingState(ReadingStateModel.ReadingState.STATE_READING); + } + getView().launchPlayer(book); + } + + private void storeDownloadedBook(boolean forAudiobook) { + BookModel stored = storedBook == null ? book.getStorageModel(bookSlug) : storedBook; + if (forAudiobook) { + List mediaUrls = book.getAudiobookFilesUrls(); + stored.setAudioFileUrls(mediaUrls); + stored.setTotalAudioChapters(mediaUrls.size()); + } else { + stored.setEbookFileUrl(book.getEpub()); + } + if (storedBook == null) { + storedBook = stored; + storage.add(storedBook); + } else { + storage.update(stored); + } + } + + public void showFavouriteButton(BookDetailsModel book) { + if(preferences.isUserLoggedIn()) { + getView().showFavouriteButton(book); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookType.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookType.java new file mode 100644 index 0000000..fd576be --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookType.java @@ -0,0 +1,21 @@ +package com.moiseum.wolnelektury.view.book; + +/** + * Created by Piotr Ostrowski on 23.08.2018. + */ +public enum BookType { + TYPE_DEFAULT { + @Override + public boolean shouldShowPremiumLock() { + return false; + } + }, + TYPE_PREMIUM { + @Override + public boolean shouldShowPremiumLock() { + return true; + } + }; + + public abstract boolean shouldShowPremiumLock(); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookView.java new file mode 100644 index 0000000..217f748 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookView.java @@ -0,0 +1,41 @@ +package com.moiseum.wolnelektury.view.book; + +import android.content.Context; + +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.view.book.components.ProgressDownloadButton; + +/** + * Created by Piotr Ostrowski on 17.11.2017. + */ + +public interface BookView { + + void initializeBookView(BookDetailsModel book); + + void changeDownloadButtonState(ProgressDownloadButton.ProgressDownloadButtonState state, boolean forAudiobook); + + void showCurrentStateProgress(int percentage, boolean forAudiobook); + + void showInitializationError(); + + void showDownloadFileError(); + + void startShareActivity(String shareUrl); + + Context getContext(); + + void openBook(String downloadedBookUrl); + + void launchPlayer(BookDetailsModel book); + + void updateReadingProgress(int currentChapter, int count, boolean forAudiobook); + + void startLikeClicked(); + + void stopLikeClicked(); + + void showFavouriteButton(BookDetailsModel book); + + void showPremiumLock(boolean lock); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/components/ProgressDownloadButton.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/components/ProgressDownloadButton.java new file mode 100644 index 0000000..5d177d8 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/components/ProgressDownloadButton.java @@ -0,0 +1,321 @@ +package com.moiseum.wolnelektury.view.book.components; + +/** + * Created by Piotr Ostrowski on 21.11.2017. + */ + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.view.View; + +import com.moiseum.wolnelektury.R; + +import java.util.Locale; + +public class ProgressDownloadButton extends View { + + private static final int MAX_PROGRESS_VALUE = 100; + + public enum ProgressDownloadButtonState { + STATE_INITIAL { + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean isDownloaded() { + return false; + } + + @Override + public boolean isDeletable() { + return false; + } + + @Override + public void draw(Canvas canvas, ProgressDownloadButtonAttributes attributes, int width, int height) { + int posY = getPosY(attributes, height); + canvas.drawText(attributes.initialText, attributes.paddingStart, posY, attributes.textPaint); + int top = (height - attributes.iconBitmap.getHeight()) / 2; + canvas.drawBitmap(attributes.iconBitmap, width - attributes.iconBitmap.getWidth() - attributes.paddingEnd, top, attributes + .bitmapPaint); + } + }, STATE_DOWNLOADING { + @Override + public boolean isEnabled() { + return false; + } + + @Override + public boolean isDownloaded() { + throw new IllegalStateException("This method shall not be called within this state"); + } + + @Override + public boolean isDeletable() { + return false; + } + + @Override + public void draw(Canvas canvas, ProgressDownloadButtonAttributes attributes, int width, int height) { + int posY = getPosY(attributes, height); + String text = String.format(Locale.getDefault(), "%s: %d%%", attributes.loadingText, attributes.currentProgress); + + // Draw text to overlap. + canvas.drawText(text, attributes.paddingStart, posY, attributes.textPaint); + + // Draw icon to overlap. + int top = (height - attributes.iconBitmap.getHeight()) / 2; + canvas.drawBitmap(attributes.iconBitmap, width - attributes.iconBitmap.getWidth() - attributes.paddingEnd, top, attributes + .bitmapPaint); + + // Draw progress + int currentProgress = attributes.currentProgress; + if (currentProgress >= 0 && currentProgress <= MAX_PROGRESS_VALUE) { + attributes.baseRect.right = attributes.baseRect.width() * currentProgress / MAX_PROGRESS_VALUE; + canvas.clipRect(attributes.baseRect); + } + + // Draw current state. + canvas.drawRoundRect(attributes.outerRectF, attributes.cornerRadius, attributes.cornerRadius, attributes.textPaint); + canvas.drawText(text, attributes.paddingStart, posY, attributes.invertedPaint); + canvas.drawBitmap(attributes.iconBitmap, width - attributes.iconBitmap.getWidth() - attributes.paddingEnd, top, attributes + .bitmapInvertedPaint); + } + }, STATE_READING { + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean isDownloaded() { + return true; + } + + @Override + public boolean isDeletable() { + return true; + } + + @Override + public void draw(Canvas canvas, ProgressDownloadButtonAttributes attributes, int width, int height) { + int posY = getPosY(attributes, height); + int top = (height - attributes.iconBitmap.getHeight()) / 2; + String text = String.format(Locale.getDefault(), "%1$s: %2$d/%3$d", attributes.downloadedText, attributes + .currentReadPosition, attributes.totalReadCount); + + canvas.drawRoundRect(attributes.outerRectF, attributes.cornerRadius, attributes.cornerRadius, attributes.textPaint); + canvas.drawText(text, attributes.paddingStart, posY, attributes.invertedPaint); + canvas.drawBitmap(attributes.iconBitmap, width - attributes.iconBitmap.getWidth() - attributes.paddingEnd, top, attributes + .bitmapInvertedPaint); + } + }; + + private static int getPosY(ProgressDownloadButtonAttributes attributes, int height) { + return (int) ((height / 2) - ((attributes.textPaint.descent() + attributes.textPaint.ascent()) / 2)); + } + + public abstract boolean isEnabled(); + + public abstract boolean isDownloaded(); + + public abstract boolean isDeletable(); + + public abstract void draw(Canvas canvas, ProgressDownloadButtonAttributes attributes, int width, int height); + } + + private static class ProgressDownloadButtonAttributes { + private String initialText = ""; + private String downloadedText = ""; + private String loadingText; + + private Rect baseRect = new Rect(); + private RectF outerRectF = new RectF(); + private RectF innerRectF = new RectF(); + + private int currentProgress = 0; + private int currentReadPosition; + private int totalReadCount; + + private Paint textPaint; + private Paint invertedPaint; + private Paint bitmapPaint; + private Paint bitmapInvertedPaint; + + private Bitmap iconBitmap; + private int cornerRadius; + private int innerCornerRadius; + private int borderSize; + private int paddingStart; + private int paddingEnd; + } + + + private ProgressDownloadButtonState currentState = ProgressDownloadButtonState.STATE_INITIAL; + private ProgressDownloadButtonAttributes attributes; + + public ProgressDownloadButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initComponents(context, attrs, defStyle, 0); + } + + public ProgressDownloadButton(Context context, AttributeSet attrs) { + super(context, attrs); + initComponents(context, attrs, 0, 0); + } + + public ProgressDownloadButton(Context context) { + super(context); + } + + // public InvertedTextProgressbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + // super(context, attrs, defStyleAttr, defStyleRes); + // initComponents(context, attrs, defStyleAttr, defStyleRes); + // } + + /** + * Initializes the text paint. This has a fix size. + * + * @param attrs The XML attributes to use. + */ + @SuppressLint("ResourceType") + private void initComponents(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + attributes = new ProgressDownloadButtonAttributes(); + + TypedArray baseAttributes = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.paddingStart, android.R.attr + .paddingEnd}); + TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.ProgressDownloadButton, defStyleAttr, defStyleRes); + + attributes.paddingStart = baseAttributes.getDimensionPixelOffset(0, 0); + attributes.paddingEnd = baseAttributes.getDimensionPixelOffset(1, 0); + + Paint textPaint = new Paint(); + textPaint.setColor(styledAttributes.getColor(R.styleable.ProgressDownloadButton_text_color, Color.BLACK)); + textPaint.setStyle(Paint.Style.FILL); + textPaint.setTextSize(styledAttributes.getDimensionPixelSize(R.styleable.ProgressDownloadButton_text_size, context.getResources() + .getDimensionPixelSize(R.dimen.download_button_text_size_default))); + textPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); + textPaint.setTextAlign(Paint.Align.LEFT); // Text draw is started in the middle + textPaint.setLinearText(true); + textPaint.setAntiAlias(true); + + // Define the inverted text paint. + Paint invertedPaint = new Paint(textPaint); + invertedPaint.setColor(styledAttributes.getColor(R.styleable.ProgressDownloadButton_text_inverted_color, Color.WHITE)); + attributes.textPaint = textPaint; + attributes.invertedPaint = invertedPaint; + + // Define paint for drawable + Paint bitmapPaint = new Paint(); + bitmapPaint.setColorFilter(new PorterDuffColorFilter(styledAttributes.getColor(R.styleable.ProgressDownloadButton_text_color, + Color.BLACK), PorterDuff.Mode.SRC_ATOP)); + attributes.bitmapPaint = bitmapPaint; + + // Define paint for inverted drawable + Paint bitmapInvertedPaint = new Paint(); + bitmapInvertedPaint.setColorFilter(new PorterDuffColorFilter(styledAttributes.getColor(R.styleable + .ProgressDownloadButton_text_inverted_color, Color.WHITE), PorterDuff.Mode.SRC_ATOP)); + attributes.bitmapInvertedPaint = bitmapInvertedPaint; + + // Define the text. + String initialText = styledAttributes.getString(R.styleable.ProgressDownloadButton_text_initial); + if (initialText != null) { + initialText = initialText.toUpperCase(); + attributes.initialText = initialText; + } + String downloadedText = styledAttributes.getString(R.styleable.ProgressDownloadButton_text_downloaded); + if (downloadedText != null) { + downloadedText = downloadedText.toUpperCase(); + attributes.downloadedText = downloadedText; + } + attributes.loadingText = context.getString(R.string.download_ebook_loading); + + // Load drawable + attributes.iconBitmap = BitmapFactory.decodeResource(getResources(), styledAttributes.getResourceId(R.styleable + .ProgressDownloadButton_drawable, android.R.drawable.ic_delete)); + + attributes.borderSize = styledAttributes.getDimensionPixelSize(R.styleable.ProgressDownloadButton_border_size, context + .getResources().getDimensionPixelSize(R.dimen.download_button_border_size_default)); + attributes.cornerRadius = styledAttributes.getDimensionPixelSize(R.styleable.ProgressDownloadButton_corner_radius, context + .getResources().getDimensionPixelSize(R.dimen.download_button_corner_radius_default)); + attributes.innerCornerRadius = attributes.cornerRadius - attributes.borderSize; + + // Recycle the TypedArray. + baseAttributes.recycle(); + styledAttributes.recycle(); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.getClipBounds(attributes.baseRect); + RectF outerRectF = attributes.outerRectF; + int borderSize = attributes.borderSize; + outerRectF.set(attributes.baseRect); + attributes.innerRectF.set(outerRectF.left + borderSize, outerRectF.top + borderSize, outerRectF.right - borderSize, outerRectF + .bottom - borderSize); + + // Draw outline + canvas.drawRoundRect(outerRectF, attributes.cornerRadius, attributes.cornerRadius, attributes.textPaint); + canvas.drawRoundRect(attributes.innerRectF, attributes.innerCornerRadius, attributes.innerCornerRadius, attributes.invertedPaint); + + // Draw current state + getState().draw(canvas, attributes, getWidth(), getHeight()); + } + + public void setState(ProgressDownloadButtonState state) { + this.currentState = state; + this.setEnabled(state.isEnabled()); + invalidate(); + } + + public ProgressDownloadButtonState getState() { + return currentState; + } + + /** + * Sets the text that will overlay. + * + * @param text The text to draw. + */ + public void setText(String text) { + attributes.initialText = text; + } + + /** + * Gets the current text to draw. + * + * @return The current text to draw. + */ + public String getText() { + return attributes.initialText; + } + + public int getCurrentProgress() { + return attributes.currentProgress; + } + + public void setProgress(int progress) { + attributes.currentProgress = progress; + invalidate(); + } + + public void setCurrentReadCount(int position, int count) { + attributes.currentReadPosition = position; + attributes.totalReadCount = count; + invalidate(); + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/AudiobooksDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/AudiobooksDataProvider.java new file mode 100644 index 0000000..f4d0a42 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/AudiobooksDataProvider.java @@ -0,0 +1,24 @@ +package com.moiseum.wolnelektury.view.book.list; + +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.services.BooksService; + +import java.util.List; + +import retrofit2.Call; + + +public class AudiobooksDataProvider extends DataProvider, BooksService> { + + @Override + public Call> execute(BooksService service) { + return service.getAudiobooks(lastKeySlug , RestClient.PAGINATION_LIMIT); + } + + @Override + protected Class getServiceClass() { + return BooksService.class; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListActivity.java new file mode 100644 index 0000000..7b38560 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListActivity.java @@ -0,0 +1,44 @@ +package com.moiseum.wolnelektury.view.book.list; + +import android.content.Context; +import android.os.Bundle; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.base.AbstractIntent; + +import static com.moiseum.wolnelektury.view.book.list.BookListActivity.BookListIntent.PARAM_LIST_TYPE; + + +public class BookListActivity extends AbstractActivity { + + public static final String BOOK_LIST_FRAGMENT_TAG = "BookListFragmentTag"; + + public static class BookListIntent extends AbstractIntent { + + static final String PARAM_LIST_TYPE = "PARAM_LIST_TYPE"; + + public BookListIntent(BookListType type, Context context) { + super(context, BookListActivity.class); + putExtra(PARAM_LIST_TYPE, type); + } + + } + + @Override + public int getLayoutResourceId() { + return R.layout.activity_blank; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + BookListType type = (BookListType) getIntent().getSerializableExtra(PARAM_LIST_TYPE); + setTitle(type.getActivityTitle()); + + BooksListFragment bookListFragment = (BooksListFragment) getSupportFragmentManager().findFragmentByTag(BOOK_LIST_FRAGMENT_TAG); + if (bookListFragment == null) { + BooksListFragment fragment = BooksListFragment.newInstance(type); + getSupportFragmentManager().beginTransaction().add(R.id.flContainer, fragment, BOOK_LIST_FRAGMENT_TAG).commit(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListType.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListType.java new file mode 100644 index 0000000..9e2b326 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListType.java @@ -0,0 +1,292 @@ +package com.moiseum.wolnelektury.view.book.list; + +import android.support.annotation.StringRes; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.ReadingStateModel; +import com.moiseum.wolnelektury.connection.services.BooksService; + +import java.util.List; + +/** + * @author golonkos + */ + +public enum BookListType { + + DOWNLOADED { + @Override + public DataProvider, BooksService> getDataProvider() { + return new DownloadedBooksDataProvider(); + } + + @Override + public boolean isDeletable() { + return true; + } + + @Override + public boolean isSearchable() { + return false; + } + + @Override + public boolean isPageable() { + return false; + } + + @Override + public String getNameForTracker() { + return "DownloadedList"; + } + + @Override + public int getActivityTitle() { + return R.string.nav_downloaded; + } + + @Override + public int getEmptyListText() { + return R.string.downloaded_empty_list; + } + }, + + AUDIOBOOKS { + @Override + public DataProvider, BooksService> getDataProvider() { + return new AudiobooksDataProvider(); + } + + @Override + public boolean isDeletable() { + return false; + } + + @Override + public boolean isSearchable() { + return true; + } + + @Override + public boolean isPageable() { + return true; + } + + @Override + public String getNameForTracker() { + return "AudiobooksList"; + } + + @Override + public int getActivityTitle() { + return R.string.nav_audiobooks; + } + + @Override + public int getEmptyListText() { + return R.string.audiobooks_empty_list; + } + }, + + NEWEST { + @Override + public DataProvider, BooksService> getDataProvider() { + return new NewestBooksDataProvider(); + } + + @Override + public boolean isDeletable() { + return false; + } + + @Override + public boolean isSearchable() { + return false; + } + + @Override + public boolean isPageable() { + return false; + } + + @Override + public String getNameForTracker() { + return "NewestList"; + } + + @Override + public int getActivityTitle() { + return R.string.book_list_newest_title; + } + + @Override + public int getEmptyListText() { + return R.string.newest_empty_list; + } + }, + + RECOMMENDED { + @Override + public DataProvider, BooksService> getDataProvider() { + return new RecommendedBooksDataProvider(); + } + + @Override + public boolean isDeletable() { + return false; + } + + @Override + public boolean isSearchable() { + return false; + } + + @Override + public boolean isPageable() { + return false; + } + + @Override + public String getNameForTracker() { + return "RecommendedList"; + } + + @Override + public int getActivityTitle() { + return R.string.book_list_recommended_title; + } + + @Override + public int getEmptyListText() { + return R.string.recommended_empty_list; + } + }, + + READING { + @Override + public DataProvider, BooksService> getDataProvider() { + return new ReadingStateDataProvider(ReadingStateModel.ReadingState.STATE_READING); + } + + @Override + public boolean isDeletable() { + return false; + } + + @Override + public boolean isSearchable() { + return false; + } + + @Override + public boolean isPageable() { + return true; + } + + @Override + public String getNameForTracker() { + return "NowReadingList"; + } + + @Override + public int getActivityTitle() { + return R.string.nav_reading; + } + + @Override + public int getEmptyListText() { + return R.string.reading_empty_list; + } + }, + + FAVOURITES { + @Override + public DataProvider, BooksService> getDataProvider() { + return new FavouritesDataProvider(); + } + @Override + public boolean isDeletable() { + return false; + } + + @Override + public boolean isSearchable() { + return false; + } + + @Override + public boolean isPageable() { + return true; + } + + @Override + public String getNameForTracker() { + return "FavouritesList"; + } + + @Override + public int getActivityTitle() { + return R.string.nav_favourites; + } + + @Override + public int getEmptyListText() { + return R.string.faviourites_empty_list; + } + }, + + COMPLETED { + @Override + public DataProvider, BooksService> getDataProvider() { + return new ReadingStateDataProvider(ReadingStateModel.ReadingState.STATE_COMPLETED); + } + + @Override + public boolean isDeletable() { + return false; + } + + @Override + public boolean isSearchable() { + return false; + } + + @Override + public boolean isPageable() { + return true; + } + + @Override + public String getNameForTracker() { + return "CompletedList"; + } + + @Override + public int getActivityTitle() { + return R.string.nav_completed; + } + + @Override + public int getEmptyListText() { + return R.string.completed_empty_list; + } + }; + + public abstract DataProvider, BooksService> getDataProvider(); + + public abstract boolean isDeletable(); + + public abstract boolean isSearchable(); + + public abstract boolean isPageable(); + + public abstract String getNameForTracker(); + + @StringRes + public abstract int getActivityTitle(); + + @StringRes + public abstract int getEmptyListText(); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListAdapter.java new file mode 100644 index 0000000..07e3f7f --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListAdapter.java @@ -0,0 +1,159 @@ +package com.moiseum.wolnelektury.view.book.list; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.support.annotation.NonNull; +import android.text.SpannableString; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter; +import com.moiseum.wolnelektury.components.recycler.ViewHolder; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; + +import butterknife.BindView; + +/** + * Created by piotrostrowski on 16.11.2017. + */ + +public class BooksListAdapter extends RecyclerAdapter { + + public interface BooksListDeletionListener { + void onDeleteBookClicked(BookModel book, int position); + } + + private BooksListDeletionListener listener; + private View.OnClickListener deleteButtonClick = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + int position = (int) v.getTag(); + BookModel book = getItem(position); + listener.onDeleteBookClicked(book, position); + } + } + }; + + public BooksListAdapter(Context context) { + super(context, RecyclerAdapter.Selection.NONE); + } + + @Override + protected String getItemId(BookModel item) { + return item.getSlug(); + } + + @NonNull + @Override + public BookViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new BookViewHolder(inflate(R.layout.list_search, parent)); + } + + @Override + public void onBindViewHolder(BookViewHolder viewHolder, int position) { + super.onBindViewHolder(viewHolder, position); + viewHolder.ibDeleteEbook.setTag(position); + viewHolder.ibDeleteEbook.setOnClickListener(deleteButtonClick); + } + + public void setOnDeleteListener(BooksListDeletionListener listener) { + this.listener = listener; + } + + static class BookViewHolder extends ViewHolder { + + @BindView(R.id.ivBookCover) + ImageView ivBookCover; + @BindView(R.id.tvBookAuthor) + TextView tvBookAuthor; + @BindView(R.id.tvBookTitle) + TextView tvBookTitle; + @BindView(R.id.tvBookEpoch) + TextView tvBookEpoch; + @BindView(R.id.tvBookGenre) + TextView tvBookGenre; + @BindView(R.id.tvBookKind) + TextView tvBookKind; + @BindView(R.id.ibDeleteEbook) + ImageButton ibDeleteEbook; + @BindView(R.id.ivEbook) + ImageView ivEbook; + @BindView(R.id.tvEbookReaden) + TextView tvEbookReaden; + @BindView(R.id.ivAudioBook) + ImageView ivAudioBook; + @BindView(R.id.tvAudioBookReaden) + TextView tvAudioBookReaden; + private final SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + + BookViewHolder(View view) { + super(view); + } + + @Override + public void bind(BookModel item, boolean selected) { + if (item.getCoverThumb() != null) { + String coverUrl = item.getCoverThumb(); + if (!coverUrl.contains(RestClient.MEDIA_URL) && !coverUrl.contains(RestClient.MEDIA_URL_HTTPS)) { + coverUrl = RestClient.MEDIA_URL_HTTPS + coverUrl; + } + Glide.with(getContext()).load(coverUrl).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivBookCover); + } else { + ivBookCover.setImageResource(R.drawable.list_nocover); + } + tvBookAuthor.setText(item.getAuthor()); + tvBookTitle.setText(item.getTitle()); + tvBookEpoch.setText(item.getEpoch()); + tvBookGenre.setText(item.getGenre()); + tvBookKind.setText(item.getKind()); + ibDeleteEbook.setVisibility(item.isDeletable() ? View.VISIBLE : View.GONE); + if (item.getCurrentChapter() != 0 && item.getTotalChapters() != 0) { + SpannableString progressSpannable = spanStringSize(item, false, R.string.reading_progress); + tvEbookReaden.setVisibility(View.VISIBLE); + tvEbookReaden.setText(progressSpannable); + } else { + tvEbookReaden.setVisibility(View.GONE); + } + if (item.isHasAudio()) { + ivAudioBook.setVisibility(View.VISIBLE); + if (item.getCurrentAudioChapter() != 0 && item.getTotalAudioChapters() != 0) { + SpannableString progressSpannable = spanStringSize(item, true, R.string.listening_progress); + tvAudioBookReaden.setVisibility(View.VISIBLE); + tvAudioBookReaden.setText(progressSpannable); + } else { + tvAudioBookReaden.setVisibility(View.GONE); + } + } else { + ivAudioBook.setVisibility(View.GONE); + tvAudioBookReaden.setVisibility(View.GONE); + } + } + + private SpannableString spanStringSize(BookModel item, boolean isAudioPart, int resourceId) { + float currentChapter = isAudioPart ? (float) item.getCurrentAudioChapter() : (float) item.getCurrentChapter(); + float totalChapter = isAudioPart ? (float) item.getTotalAudioChapters() : (float) item.getTotalChapters(); + int progress = (int) ((currentChapter / totalChapter) * 100.f); + + String progressText = getContext().getString(resourceId, progress); + SpannableString progressSpannable = new SpannableString(progressText.toUpperCase()); + int size = ((int) getContext().getResources().getDimension(R.dimen.list_title_text_size)); + progressSpannable.setSpan(new AbsoluteSizeSpan(size), progressText.indexOf(" "), progressText.length() - 1, 0); // set size + return progressSpannable; + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListFragment.java new file mode 100644 index 0000000..0bb96fb --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListFragment.java @@ -0,0 +1,184 @@ +package com.moiseum.wolnelektury.view.book.list; + +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.Toast; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.components.ProgressRecyclerView; +import com.moiseum.wolnelektury.components.recycler.EndlessRecyclerOnScrollListener; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.view.book.BookActivity; +import com.moiseum.wolnelektury.view.book.BookType; +import com.moiseum.wolnelektury.view.main.MainActivity; +import com.moiseum.wolnelektury.view.main.NavigationElement; + +import org.greenrobot.eventbus.EventBus; + +import java.util.List; + +import butterknife.BindView; +import butterknife.OnClick; + +/** + * @author golonkos + */ + +public class BooksListFragment extends PresenterFragment implements BooksListView { + + private static final String PARAM_LIST_TYPE = "PARAM_LIST_TYPE"; + + @BindView(R.id.rvBooksList) + ProgressRecyclerView rvBooksList; + @BindView(R.id.pbLoadMore) + ProgressBar pbLoadMore; + @BindView(R.id.btnReloadMore) + Button btnReloadMore; + + public static BooksListFragment newInstance(BookListType type) { + BooksListFragment fragment = new BooksListFragment(); + Bundle args = new Bundle(1); + args.putSerializable(PARAM_LIST_TYPE, type); + fragment.setArguments(args); + return fragment; + } + + private BookListType type; + private BooksListAdapter adapter; + + private EndlessRecyclerOnScrollListener rvBooksScrollListener = new EndlessRecyclerOnScrollListener() { + @Override + public void onLoadMore() { + if (adapter.getItemCount() > 0) { + getPresenter().loadMoreBooks(); + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + if (getArguments() == null || getArguments().getSerializable(PARAM_LIST_TYPE) == null) { + throw new IllegalStateException("Missing list type parameter."); + } + type = (BookListType) getArguments().getSerializable(PARAM_LIST_TYPE); + super.onCreate(savedInstanceState); + } + + @Override + protected BooksListPresenter createPresenter() { + return new BooksListPresenter(this, type); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_books_list; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + rvBooksList.setLayoutManager(layoutManager); + rvBooksList.addOnScrollListener(rvBooksScrollListener); + + adapter = new BooksListAdapter(getContext()); + adapter.setOnItemClickListener((item, view1, position) -> getPresenter().onBookClicked(item, position)); + if (type.isDeletable()) { + adapter.setOnDeleteListener((book, position) -> { + getPresenter().onBookDeleteClicked(book); + adapter.removeItem(position); + Toast.makeText(getContext() ,getString(R.string.book_deleted_message), Toast.LENGTH_SHORT).show(); + }); + } + rvBooksList.setAdapter(adapter); + rvBooksList.setEmptyText(type.getEmptyListText()); + if (type.isSearchable()) { + setHasOptionsMenu(true); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (type.isSearchable()) { + inflater.inflate(R.menu.menu_searchable, menu); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_search) { + EventBus.getDefault().post(new MainActivity.ChangeNavigationEvent(NavigationElement.SEARCH)); + } + return super.onOptionsItemSelected(item); + } + + @Override + protected String getNameForTracker() { + return type.getNameForTracker(); + } + + @Override + public void setData(List books) { + if (adapter.getItemCount() == 0) { + rvBooksList.setItems(books); + } else { + rvBooksList.addItems(books); + } + } + + @Override + public void clearList() { + adapter.clear(); + } + + @Override + public void setProgressVisible(boolean visible) { + if (adapter.getItemCount() == 0) { + rvBooksList.setProgressVisible(visible); + } else { + pbLoadMore.setVisibility(visible ? View.VISIBLE : View.GONE); + } + } + + @Override + public void showError(Exception e) { + Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show(); + if (adapter.getItemCount() != 0) { + btnReloadMore.setVisibility(View.VISIBLE); + } else { + rvBooksList.showRetryButton(() -> getPresenter().reloadBooks()); + } + } + + @Override + public void openBookDetailsView(String bookSlug) { + startActivity(new BookActivity.BookIntent(bookSlug, BookType.TYPE_DEFAULT, getActivity())); + } + + @Override + public void updateEmptyViewVisibility() { + rvBooksList.updateEmptyViewVisibility(); + } + + @Override + public void updateFavouriteState(boolean state, Integer clickedPosition) { + if (clickedPosition != null) { + adapter.getItem(clickedPosition).setLiked(state); + adapter.notifyDataSetChanged(); + } + } + + @OnClick(R.id.btnReloadMore) + public void onReloadMoreClicked() { + btnReloadMore.setVisibility(View.GONE); + pbLoadMore.setVisibility(View.VISIBLE); + getPresenter().loadMoreBooks(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListPresenter.java new file mode 100644 index 0000000..030bbea --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListPresenter.java @@ -0,0 +1,185 @@ +package com.moiseum.wolnelektury.view.book.list; + +import android.os.Bundle; + +import com.moiseum.wolnelektury.base.DataObserver; +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.downloads.FileCacheUtils; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.services.BooksService; +import com.moiseum.wolnelektury.storage.BookStorage; +import com.moiseum.wolnelektury.events.BookFavouriteEvent; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +/** + * @author golonkos + */ + +class BooksListPresenter extends FragmentPresenter { + + private static final String TAG = BooksListPresenter.class.getSimpleName(); + + private final DataProvider, BooksService> dataProvider; + private BookStorage storage; + private final BookListType bookListType; + private String lastKey; + private Integer clickedPosition; + + BooksListPresenter(BooksListView view, BookListType type) { + super(view); + bookListType = type; + lastKey = null; + dataProvider = type.getDataProvider(); + dataProvider.setDataObserver(new BooksListDataObserver()); + storage = WLApplication.getInstance().getBookStorage(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EventBus.getDefault().register(this); + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + loadBooks(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + dataProvider.cancel(); + EventBus.getDefault().unregister(this); + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onBookAdded(BookStorage.BookAddedEvent event) { + if (bookListType == BookListType.DOWNLOADED) { + reloadBooks(); + } + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onBookDeleted(BookStorage.BookDeletedEvent event) { + if (bookListType == BookListType.DOWNLOADED) { + reloadBooks(); + } + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFavouriteStateChanged(BookFavouriteEvent event) { + // TODO: Change that full reload + if (bookListType == BookListType.FAVOURITES) { + reloadBooks(); + } else { + getView().updateFavouriteState(event.getState(), clickedPosition); + } + } + + public void reloadBooks() { + lastKey = null; + getView().clearList(); + loadBooks(); + } + + public void loadMoreBooks() { + if (bookListType.isPageable()) { + loadBooks(); + } + } + + private void loadBooks() { + dataProvider.load(lastKey); + } + + void onBookClicked(BookModel book, int position) { + clickedPosition = position; + getView().openBookDetailsView(book.getSlug()); + } + + void onBookDeleteClicked(BookModel book) { + BookModel downloadedBook = storage.find(book.getSlug()); + + List deletionOperations = new ArrayList<>(); + if (downloadedBook != null && downloadedBook.isEbookDownloaded()) { + deletionOperations.add(FileCacheUtils.deleteEbookFile(downloadedBook.getEbookFileUrl())); + } + if (downloadedBook != null && downloadedBook.isAudioDownloaded()) { + deletionOperations.add(FileCacheUtils.deleteAudiobookFiles(downloadedBook.getAudioFileUrls())); + } + + storage.remove(book.getSlug(), false); + addDisposable(Completable.concat(deletionOperations).andThen(Completable.fromAction(() -> { + })).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(() -> getView().updateEmptyViewVisibility(), error -> { + getView().showError(new Exception(error)); + //restore books + loadBooks(); + })); + } + + private class BooksListDataObserver implements DataObserver> { + + private List matchDownloadedBooks(List books) { + if (bookListType.isDeletable()) { + List merged = new ArrayList<>(books.size()); + List downloadedBooks = storage.all(); + + for (BookModel book : books) { + boolean found = false; + for (BookModel downloaded : downloadedBooks) { + if (book.getSlug().equals(downloaded.getSlug())) { + merged.add(downloaded); + found = true; + break; + } + } + if (!found) { + merged.add(book); + } + } + return merged; + } else { + return books; + } + } + + @Override + public void onLoadStarted() { + getView().setProgressVisible(true); + } + + @Override + public void onLoadSuccess(List data) { + if (data.size() > 0) { + lastKey = data.get(data.size() - 1).getSortedKey(); + data = matchDownloadedBooks(data); + } + getView().setProgressVisible(false); + getView().setData(data); + } + + @Override + public void onLoadFailed(Exception e) { + getView().setProgressVisible(false); + getView().setData(Collections.emptyList()); + getView().showError(e); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListView.java new file mode 100644 index 0000000..c6249f4 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListView.java @@ -0,0 +1,19 @@ +package com.moiseum.wolnelektury.view.book.list; + +import com.moiseum.wolnelektury.base.mvp.LoadingView; +import com.moiseum.wolnelektury.connection.models.BookModel; + +import java.util.List; + +/** + * @author golonkos + */ +interface BooksListView extends LoadingView> { + void openBookDetailsView(String bookSlug); + + void updateEmptyViewVisibility(); + + void updateFavouriteState(boolean state, Integer clickedPosition); + + void clearList(); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/DownloadedBooksDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/DownloadedBooksDataProvider.java new file mode 100644 index 0000000..81b65cf --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/DownloadedBooksDataProvider.java @@ -0,0 +1,35 @@ +package com.moiseum.wolnelektury.view.book.list; + +import com.moiseum.wolnelektury.storage.BookStorage; +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.services.BooksService; + +import java.util.List; + +import retrofit2.Call; + +/** + * @author golonkos + */ +public class DownloadedBooksDataProvider extends DataProvider, BooksService> { + + @Override + public void load(String lastKey) { + if (dataObserver != null && lastKey == null) { + BookStorage bookStorage = WLApplication.getInstance().getBookStorage(); + dataObserver.onLoadSuccess(bookStorage.all()); + } + } + + @Override + public Call> execute(BooksService service) { + return null; + } + + @Override + protected Class getServiceClass() { + return null; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/FavouritesDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/FavouritesDataProvider.java new file mode 100644 index 0000000..4634ba6 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/FavouritesDataProvider.java @@ -0,0 +1,23 @@ +package com.moiseum.wolnelektury.view.book.list; + +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.services.BooksService; + +import java.util.List; + +import retrofit2.Call; + +public class FavouritesDataProvider extends DataProvider,BooksService> { + + @Override + public Call> execute(BooksService service) { + return service.getFavourites(lastKeySlug , RestClient.PAGINATION_LIMIT); + } + + @Override + protected Class getServiceClass() { + return BooksService.class; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/NewestBooksDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/NewestBooksDataProvider.java new file mode 100644 index 0000000..df703e9 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/NewestBooksDataProvider.java @@ -0,0 +1,26 @@ +package com.moiseum.wolnelektury.view.book.list; + +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.services.BooksService; + +import java.util.List; + +import retrofit2.Call; + +/** + * @author golonkos + */ + +public class NewestBooksDataProvider extends DataProvider, BooksService> { + + @Override + public Call> execute(BooksService service) { + return service.getNewest(); + } + + @Override + protected Class getServiceClass() { + return BooksService.class; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/ReadingStateDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/ReadingStateDataProvider.java new file mode 100644 index 0000000..356c920 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/ReadingStateDataProvider.java @@ -0,0 +1,33 @@ +package com.moiseum.wolnelektury.view.book.list; + +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.ReadingStateModel; +import com.moiseum.wolnelektury.connection.services.BooksService; + +import java.util.List; + +import retrofit2.Call; + +/** + * Created by Piotr Ostrowski on 24.06.2018. + */ +public class ReadingStateDataProvider extends DataProvider, BooksService> { + + private ReadingStateModel.ReadingState state; + + public ReadingStateDataProvider(ReadingStateModel.ReadingState state) { + this.state = state; + } + + @Override + protected Class getServiceClass() { + return BooksService.class; + } + + @Override + public Call> execute(BooksService service) { + return service.getReadenBooks(state.getStateName(), lastKeySlug, RestClient.PAGINATION_LIMIT); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/RecommendedBooksDataProvider.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/RecommendedBooksDataProvider.java new file mode 100644 index 0000000..a525498 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/RecommendedBooksDataProvider.java @@ -0,0 +1,26 @@ +package com.moiseum.wolnelektury.view.book.list; + +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.services.BooksService; + +import java.util.List; + +import retrofit2.Call; + +/** + * @author golonkos + */ + +public class RecommendedBooksDataProvider extends DataProvider, BooksService> { + + @Override + public Call> execute(BooksService service) { + return service.getRecommended(); + } + + @Override + protected Class getServiceClass() { + return BooksService.class; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/BookViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/BookViewHolder.java new file mode 100644 index 0000000..73c7667 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/BookViewHolder.java @@ -0,0 +1,56 @@ +package com.moiseum.wolnelektury.view.library; + +import android.graphics.Color; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.components.recycler.ViewHolder; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.models.BookModel; + +import butterknife.BindView; + +/** + * @author golonkos + */ + +class BookViewHolder extends ViewHolder { + + private static final String DEFAULT_OVERLAY_COLOR = "#80db4b16"; + + @BindView(R.id.ivBookCover) + ImageView ivBookCover; + @BindView(R.id.tvBookAuthor) + TextView tvBookAuthor; + @BindView(R.id.tvBookTitle) + TextView tvBookTitle; + @BindView(R.id.ivAudioBook) + ImageView ivAudioBook; + @BindView(R.id.llBookContent) + LinearLayout llBookContent; + + BookViewHolder(View view) { + super(view); + } + + @Override + public void bind(BookModel item, boolean selected) { + if (item.getCoverThumb() != null) { + String coverUrl = item.getCoverThumb(); + if (!coverUrl.contains(RestClient.MEDIA_URL) && !coverUrl.contains(RestClient.MEDIA_URL_HTTPS)) { + coverUrl = RestClient.MEDIA_URL_HTTPS + coverUrl; + } + Glide.with(getContext()).load(coverUrl).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivBookCover); + } + tvBookAuthor.setText(item.getAuthor()); + tvBookTitle.setText(item.getTitle()); + ivAudioBook.setVisibility(item.isHasAudio() ? View.VISIBLE : View.GONE); + String colorHash = item.getCoverColor() != null ? item.getCoverColor() : DEFAULT_OVERLAY_COLOR; + llBookContent.setBackgroundColor(Color.parseColor(colorHash)); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryAdapter.java new file mode 100644 index 0000000..4dcc724 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryAdapter.java @@ -0,0 +1,29 @@ +package com.moiseum.wolnelektury.view.library; + +import android.content.Context; +import android.view.ViewGroup; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter; +import com.moiseum.wolnelektury.connection.models.BookModel; + +/** + * @author golonkos + */ + +public class LibraryAdapter extends RecyclerAdapter { + + public LibraryAdapter(Context context) { + super(context, Selection.NONE); + } + + @Override + protected String getItemId(BookModel item) { + return null; + } + + @Override + public BookViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new BookViewHolder(inflate(R.layout.book_item, parent)); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryFragment.java new file mode 100644 index 0000000..e029f1b --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryFragment.java @@ -0,0 +1,267 @@ +package com.moiseum.wolnelektury.view.library; + +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.components.ProgressRecyclerView; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.utils.StringUtils; +import com.moiseum.wolnelektury.view.book.BookActivity; +import com.moiseum.wolnelektury.view.book.BookType; +import com.moiseum.wolnelektury.view.book.list.BookListActivity; +import com.moiseum.wolnelektury.view.book.list.BookListType; +import com.moiseum.wolnelektury.view.main.MainActivity; +import com.moiseum.wolnelektury.view.main.NavigationElement; + +import org.greenrobot.eventbus.EventBus; + +import java.util.List; + +import butterknife.BindView; +import butterknife.OnClick; + +/** + * @author golonkos + */ + +public class LibraryFragment extends PresenterFragment implements LibraryView { + + @BindView(R.id.rlReadingNowContainer) + View rlReadingNowContainer; + @BindView(R.id.rvNowReading) + ProgressRecyclerView rvNowReading; + @BindView(R.id.rvNewest) + ProgressRecyclerView rvNewest; + @BindView(R.id.rvRecommended) + ProgressRecyclerView rvRecommended; + @BindView(R.id.ivBookCover) + ImageView ivBookCover; + @BindView(R.id.tvBookAuthor) + TextView tvBookAuthor; + @BindView(R.id.tvBookTitle) + TextView tvBookTitle; + @BindView(R.id.tvBookEpoch) + TextView tvBookEpoch; + @BindView(R.id.tvBookKind) + TextView tvBookKind; + @BindView(R.id.tvBookGenre) + TextView tvBookGenre; + @BindView(R.id.pbHeaderLoading) + ProgressBar pbHeaderLoading; + @BindView(R.id.tvEmpty) + TextView tvEmpty; + @BindView(R.id.ibRetry) + ImageButton ibRetry; + @BindView(R.id.rlHeaderLoadingContainer) + RelativeLayout rlHeaderLoadingContainer; + @BindView(R.id.rlBecomeAFriend) + View rlBecomeAFriend; + @BindView(R.id.vBecomeAFriendSeparator) + View vBecomeAFriendSeparator; + @BindView(R.id.ivAudiobook) + ImageView ivHeaderAudiobook; + + public static LibraryFragment newInstance() { + return new LibraryFragment(); + } + + @Override + protected LibraryPresenter createPresenter() { + return new LibraryPresenter(this); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_library; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + setHasOptionsMenu(true); + initList(rvNowReading); + initList(rvNewest); + initList(rvRecommended); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.menu_searchable, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_search) { + EventBus.getDefault().post(new MainActivity.ChangeNavigationEvent(NavigationElement.SEARCH)); + } + return super.onOptionsItemSelected(item); + } + + @Override + public void showBecomeAFriendHeader(boolean premium) { + rlBecomeAFriend.setVisibility(premium ? View.GONE : View.VISIBLE); + vBecomeAFriendSeparator.setVisibility(premium ? View.VISIBLE : View.GONE); + } + + @Override + public void initHeader(BookDetailsModel item) { + if (item.getCoverThumb() != null) { + String coverUrl = item.getCoverThumb(); + if (!coverUrl.contains(RestClient.MEDIA_URL) && !coverUrl.contains(RestClient.MEDIA_URL_HTTPS)) { + coverUrl = RestClient.MEDIA_URL_HTTPS + coverUrl; + } + Glide.with(getContext()).load(coverUrl).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivBookCover); + } else { + ivBookCover.setImageResource(R.drawable.list_nocover); + } + tvBookAuthor.setText(StringUtils.joinCategory(item.getAuthors(), ", ")); + tvBookTitle.setText(item.getTitle()); + tvBookEpoch.setText(StringUtils.joinCategory(item.getEpochs(), ", ")); + tvBookKind.setText(StringUtils.joinCategory(item.getKinds(), ", ")); + tvBookGenre.setText(StringUtils.joinCategory(item.getGenres(), ", ")); + ivHeaderAudiobook.setVisibility(item.hasAudio() ? View.VISIBLE : View.GONE); + } + + @Override + public void setProgressContainerVisible(boolean visible) { + if (visible) { + rlHeaderLoadingContainer.setVisibility(View.VISIBLE); + } else { + rlHeaderLoadingContainer.setVisibility(View.GONE); + } + } + + @Override + public void showHeaderError() { + ibRetry.setVisibility(View.VISIBLE); + } + + @Override + public void setHeaderProgressVisible(boolean visible) { + pbHeaderLoading.setVisibility(visible ? View.VISIBLE : View.GONE); + if (visible) { + tvEmpty.setVisibility(View.GONE); + } + } + + @Override + public void showHeaderEmpty(boolean userLoggedIn) { + tvEmpty.setVisibility(View.VISIBLE); + tvEmpty.setText(userLoggedIn ? R.string.no_prapremiere_message_logged : R.string.no_prapremiere_message); + } + + private void initList(ProgressRecyclerView rvList) { + rvList.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); + rvList.setHasFixedSize(true); + rvList.setEmptyText(R.string.read_now_library_empty); + LibraryAdapter adapter = new LibraryAdapter(getContext()); + adapter.setOnItemClickListener((item, view, position) -> getPresenter().onBookClicked(item)); + rvList.setAdapter(adapter); + } + + @Override + public void setNowReadingVisibility(boolean visible) { + rlReadingNowContainer.setVisibility(visible ? View.VISIBLE : View.GONE); + rvNowReading.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + @Override + public void setNewest(List books) { + rvNewest.setItems(books); + } + + @Override + public void setNewestProgressVisible(boolean visible) { + rvNewest.setProgressVisible(visible); + } + + @Override + public void showNewestError(Exception e) { + Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show(); + rvNewest.showRetryButton(() -> getPresenter().reloadNewest()); + } + + @Override + public void setRecommended(List books) { + rvRecommended.setItems(books); + } + + @Override + public void setRecommendedProgressVisible(boolean visible) { + rvRecommended.setProgressVisible(visible); + } + + @Override + public void showRecommendedError(Exception e) { + Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show(); + rvRecommended.showRetryButton(() -> getPresenter().reloadRecommended()); + } + + @Override + public void setNowReading(List books) { + rvNowReading.setItems(books); + } + + @Override + public void setNowReadingProgressVisible(boolean visible) { + rvNowReading.setProgressVisible(visible); + } + + @Override + public void showNowReadingError(Exception e) { + Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show(); + rvNowReading.showRetryButton(() -> getPresenter().reloadNowReading()); + } + + @Override + public void openBookDetailsView(String slug, BookType bookType) { + startActivity(new BookActivity.BookIntent(slug, bookType, getContext())); + } + + @OnClick(R.id.btnBecomeAFriend) + public void onBecomeAFriendClick() { + showPayPalForm(); + } + + @OnClick(R.id.btnNewestSeeAll) + public void onNewestSeeAllClicked() { + startActivity(new BookListActivity.BookListIntent(BookListType.NEWEST, getActivity())); + } + + @OnClick(R.id.btnRecommendedSeeAll) + public void onRecommendedSeeAllClicked() { + startActivity(new BookListActivity.BookListIntent(BookListType.RECOMMENDED, getActivity())); + } + + @OnClick(R.id.btnNowReadingSeeAll) + public void onNowReadingSeeAllClicked() { + getPresenter().onNowReadingSeeAllClicked(); + } + + @OnClick(R.id.ibRetry) + public void onHeaderRetryClicked() { + getPresenter().fetchHeader(); + ibRetry.setVisibility(View.GONE); + } + + @OnClick(R.id.libraryHeader) + public void onLibraryHeaderClicked() { + getPresenter().onPremiereHeaderClicked(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryPresenter.java new file mode 100644 index 0000000..88f9b8b --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryPresenter.java @@ -0,0 +1,245 @@ +package com.moiseum.wolnelektury.view.library; + +import android.os.Bundle; + +import com.moiseum.wolnelektury.base.DataObserver; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.RestClientCallback; +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.ReadingStateModel; +import com.moiseum.wolnelektury.connection.services.BooksService; +import com.moiseum.wolnelektury.events.LoggedInEvent; +import com.moiseum.wolnelektury.storage.BookStorage; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; +import com.moiseum.wolnelektury.view.book.BookType; +import com.moiseum.wolnelektury.view.book.list.NewestBooksDataProvider; +import com.moiseum.wolnelektury.view.book.list.ReadingStateDataProvider; +import com.moiseum.wolnelektury.view.book.list.RecommendedBooksDataProvider; +import com.moiseum.wolnelektury.view.main.MainActivity; +import com.moiseum.wolnelektury.view.main.NavigationElement; +import com.moiseum.wolnelektury.view.main.events.PremiumStatusEvent; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; + +import retrofit2.Call; + +/** + * @author golonkos + */ +class LibraryPresenter extends FragmentPresenter { + + private static final String TAG = LibraryPresenter.class.getSimpleName(); + private final NewestBooksDataProvider newestBooksDataProvider; + private final RecommendedBooksDataProvider recommendedBooksDataProvider; + private final ReadingStateDataProvider nowReadingBooksDataProvider; + private BookDetailsModel premiereBook; + private Call currentCall; + private RestClient client = WLApplication.getInstance().getRestClient(); + + private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + + LibraryPresenter(LibraryView view) { + super(view); + newestBooksDataProvider = new NewestBooksDataProvider(); + newestBooksDataProvider.setDataObserver(new NewestDataObserver()); + + recommendedBooksDataProvider = new RecommendedBooksDataProvider(); + recommendedBooksDataProvider.setDataObserver(new RecommendedDataObserver()); + + nowReadingBooksDataProvider = new ReadingStateDataProvider(ReadingStateModel.ReadingState.STATE_READING); + nowReadingBooksDataProvider.setDataObserver(new NowReadingDataObserver()); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EventBus.getDefault().register(this); + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + newestBooksDataProvider.load(null); + recommendedBooksDataProvider.load(null); + if (preferences.isUserLoggedIn()) { + getView().setNowReadingVisibility(true); + nowReadingBooksDataProvider.load(null); + } + fetchHeader(); + } + + @Override + public void onResume() { + super.onResume(); + getView().showBecomeAFriendHeader(preferences.isUserPremium()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + newestBooksDataProvider.cancel(); + recommendedBooksDataProvider.cancel(); + nowReadingBooksDataProvider.cancel(); + if (currentCall != null) { + currentCall.cancel(); + } + EventBus.getDefault().unregister(this); + } + + void onNowReadingSeeAllClicked() { + EventBus.getDefault().post(new MainActivity.ChangeNavigationEvent(NavigationElement.NOW_READING)); + } + + void onBookClicked(BookModel book) { + getView().openBookDetailsView(book.getSlug(), BookType.TYPE_DEFAULT); + } + + void reloadNewest() { + newestBooksDataProvider.load(null); + } + + void reloadRecommended() { + recommendedBooksDataProvider.load(null); + } + + void reloadNowReading() { + nowReadingBooksDataProvider.load(null); + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onBookAdded(BookStorage.BookAddedEvent event) { + if (preferences.isUserLoggedIn()) { + nowReadingBooksDataProvider.load(null); + } + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onBookDeleted(BookStorage.BookDeletedEvent event) { + if (preferences.isUserLoggedIn()) { + nowReadingBooksDataProvider.load(null); + } + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPremiumChanged(PremiumStatusEvent event) { + getView().showBecomeAFriendHeader(event.isPremium()); + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onLoggedIn(LoggedInEvent event) { + getView().showBecomeAFriendHeader(preferences.isUserPremium()); + getView().setNowReadingVisibility(true); + nowReadingBooksDataProvider.load(null); + } + + void onPremiereHeaderClicked() { + if (premiereBook != null) { + getView().openBookDetailsView(premiereBook.getSlug(), BookType.TYPE_PREMIUM); + } + } + + void fetchHeader() { + getView().setProgressContainerVisible(true); + getView().setHeaderProgressVisible(true); + currentCall = client.call(new RestClientCallback, BooksService>() { + @Override + public void onSuccess(List data) { + getView().setHeaderProgressVisible(false); + if (data.size() > 0) { + premiereBook = data.get(0); + getView().initHeader(premiereBook); + getView().setProgressContainerVisible(false); + } else { + getView().showHeaderEmpty(preferences.isUserLoggedIn()); + } + } + + @Override + public void onFailure(Exception e) { + getView().showHeaderError(); + } + + @Override + public void onCancel() { + // nop. + } + + @Override + public Call> execute(BooksService service) { + return service.getPreview(); + } + }, BooksService.class); + } + + + private class NewestDataObserver implements DataObserver> { + + @Override + public void onLoadStarted() { + getView().setNewestProgressVisible(true); + } + + @Override + public void onLoadSuccess(List data) { + getView().setNewestProgressVisible(false); + getView().setNewest(data); + } + + @Override + public void onLoadFailed(Exception e) { + getView().setNewestProgressVisible(false); + getView().showNewestError(e); + } + } + + private class RecommendedDataObserver implements DataObserver> { + + @Override + public void onLoadStarted() { + getView().setRecommendedProgressVisible(true); + } + + @Override + public void onLoadSuccess(List data) { + getView().setRecommendedProgressVisible(false); + getView().setRecommended(data); + } + + @Override + public void onLoadFailed(Exception e) { + getView().setRecommendedProgressVisible(false); + getView().showRecommendedError(e); + } + } + + private class NowReadingDataObserver implements DataObserver> { + + @Override + public void onLoadStarted() { + getView().setNowReadingProgressVisible(true); + } + + @Override + public void onLoadSuccess(List data) { + getView().setNowReadingProgressVisible(false); + getView().setNowReading(data); + } + + @Override + public void onLoadFailed(Exception e) { + getView().setNowReadingProgressVisible(false); + getView().showNowReadingError(e); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryView.java new file mode 100644 index 0000000..a4955d5 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryView.java @@ -0,0 +1,47 @@ +package com.moiseum.wolnelektury.view.library; + +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.view.book.BookType; + +import java.util.List; + +/** + * @author golonkos + */ + +interface LibraryView { + void setNewest(List books); + + void setNewestProgressVisible(boolean visible); + + void showNewestError(Exception e); + + void setRecommended(List books); + + void setRecommendedProgressVisible(boolean visible); + + void showRecommendedError(Exception e); + + void setNowReadingVisibility(boolean visible); + + void setNowReading(List books); + + void setNowReadingProgressVisible(boolean visible); + + void showNowReadingError(Exception e); + + void openBookDetailsView(String slug, BookType bookType); + + void initHeader(BookDetailsModel item); + + void setProgressContainerVisible(boolean visible); + + void showHeaderError(); + + void showHeaderEmpty(boolean userLoggedIn); + + void setHeaderProgressVisible(boolean visible); + + void showBecomeAFriendHeader(boolean premium); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginActivity.java new file mode 100644 index 0000000..0df9ce7 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginActivity.java @@ -0,0 +1,76 @@ +package com.moiseum.wolnelektury.view.login; + +import android.app.ProgressDialog; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Toast; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.base.AbstractIntent; +import com.moiseum.wolnelektury.base.mvp.PresenterActivity; + +import butterknife.OnClick; + +/** + * Created by Piotr Ostrowski on 11.09.2018. + */ +public class LoginActivity extends PresenterActivity implements LoginView { + + public static class LoginIntent extends AbstractIntent { + + public LoginIntent(Context context) { + super(context, LoginActivity.class); + } + } + + private ProgressDialog progressDialog; + + @Override + public int getLayoutResourceId() { + return R.layout.activity_login; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + + } + + @Override + protected LoginPresenter createPresenter() { + return new LoginPresenter(this); + } + + @Override + public void setProgressDialogVisibility(boolean visible) { + if (visible && progressDialog == null) { + String dialogMessage = getString(R.string.main_view_progress); + progressDialog = ProgressDialog.show(this, null, dialogMessage, true, false); + } else if (!visible && progressDialog != null) { + progressDialog.hide(); + progressDialog = null; + } + } + + @Override + public void showToastMessage(int messageResId) { + Toast.makeText(this, messageResId, Toast.LENGTH_LONG).show(); + } + + @Override + public void showCustomTabsAuthentication(Uri authorizationUrl) { + showBrowserView(authorizationUrl); + finish(); + } + + @OnClick(R.id.ibBack) + public void onBackClicked() { + finish(); + } + + @OnClick(R.id.btnLogin) + public void onLoginClicked() { + getPresenter().onLoginClicked(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginPresenter.java new file mode 100644 index 0000000..c0ecd41 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginPresenter.java @@ -0,0 +1,70 @@ +package com.moiseum.wolnelektury.view.login; + +import android.net.Uri; +import android.util.Log; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.base.mvp.Presenter; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.RestClientCallback; +import com.moiseum.wolnelektury.connection.models.OAuthTokenModel; +import com.moiseum.wolnelektury.connection.services.UserService; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; + +import retrofit2.Call; + +/** + * Created by Piotr Ostrowski on 12.09.2018. + */ +public class LoginPresenter extends Presenter { + + private static final String TAG = LoginPresenter.class.getSimpleName(); + + private RestClient client = WLApplication.getInstance().getRestClient(); + private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + private Call currentCall; + + LoginPresenter(LoginView view) { + super(view); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (currentCall != null) { + currentCall.cancel(); + } + } + + protected void onLoginClicked() { + getView().setProgressDialogVisibility(true); + currentCall = client.call(new RestClientCallback() { + + @Override + public void onSuccess(OAuthTokenModel data) { + preferences.setTemporaryLoginToken(data.getToken()); + String authUrl = String.format(RestClient.WEB_OAUTH_AUTHORIZATION_URL, data.getToken()); + getView().setProgressDialogVisibility(false); + getView().showCustomTabsAuthentication(Uri.parse(authUrl)); + } + + @Override + public void onFailure(Exception e) { + Log.e(TAG, "Failed to obtain request token.", e); + getView().setProgressDialogVisibility(false); + getView().showToastMessage(R.string.login_request_token_failed); + } + + @Override + public void onCancel() { + getView().setProgressDialogVisibility(false); + } + + @Override + public Call execute(UserService service) { + return service.requestToken(); + } + }, UserService.class); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginView.java new file mode 100644 index 0000000..b734c4f --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginView.java @@ -0,0 +1,16 @@ +package com.moiseum.wolnelektury.view.login; + +import android.net.Uri; +import android.support.annotation.StringRes; + +/** + * Created by Piotr Ostrowski on 12.09.2018. + */ +public interface LoginView { + + void setProgressDialogVisibility(boolean visible); + + void showToastMessage(@StringRes int messageResId); + + void showCustomTabsAuthentication(Uri authorizationUrl); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainActivity.java new file mode 100644 index 0000000..4917561 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainActivity.java @@ -0,0 +1,305 @@ +package com.moiseum.wolnelektury.view.main; + +import android.app.ProgressDialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.StringRes; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.v4.app.FragmentManager; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractIntent; +import com.moiseum.wolnelektury.base.mvp.PresenterActivity; +import com.moiseum.wolnelektury.view.book.BookActivity; +import com.moiseum.wolnelektury.view.book.BookType; +import com.moiseum.wolnelektury.view.supportus.SupportUsActivity; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + +import java.util.List; + +import butterknife.BindView; +import butterknife.OnClick; + +import static com.moiseum.wolnelektury.view.main.MainActivity.MainIntent.RELAUNCH_MESSAGE_KEY; + +public class MainActivity extends PresenterActivity implements MainView { + + private ProgressDialog progressDialog; + + public static class MainIntent extends AbstractIntent { + + static final String RELAUNCH_MESSAGE_KEY = "RelaunchMessageKey"; + + public MainIntent(Context context) { + super(context, MainActivity.class); + } + + public MainIntent(@StringRes int relaunchMessageResId, Context context) { + super(context, MainActivity.class); + this.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + this.putExtra(RELAUNCH_MESSAGE_KEY, relaunchMessageResId); + } + } + + public static class ChangeNavigationEvent { + private final NavigationElement element; + + public ChangeNavigationEvent(NavigationElement element) { + if (element == NavigationElement.SEPARATOR || element == NavigationElement.SUPPORT_US) { + throw new IllegalArgumentException("Unsupported navigation element"); + } + this.element = element; + } + + public NavigationElement getElement() { + return element; + } + } + + @BindView(R.id.drawer_layout) + DrawerLayout drawerLayout; + @BindView(R.id.rvNavigation) + RecyclerView rvNavigation; + @BindView(R.id.btnLogin) + Button btnLogin; + @BindView(R.id.llLoggedInContainer) + View llLoggedInContainer; + @BindView(R.id.tvUsername) + TextView tvUsername; + + private ActionBarDrawerToggle drawerToggle; + private NavigationAdapter navigationAdapter; + private NavigationElement currentNavigationElement; + + @Override + public int getLayoutResourceId() { + return R.layout.activity_main; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + setBackButtonEnable(true); + initDrawer(); + EventBus.getDefault().register(this); + + if (getIntent().hasExtra(RELAUNCH_MESSAGE_KEY)) { + Toast.makeText(this, getIntent().getIntExtra(RELAUNCH_MESSAGE_KEY, 0), Toast.LENGTH_SHORT).show(); + } + } + + @Override + protected MainPresenter createPresenter() { + return new MainPresenter(this); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + String action = intent.getAction(); + String data = intent.getDataString(); + if (Intent.ACTION_VIEW.equals(action) && data != null) { + getPresenter().onBrowserCallback(data); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + EventBus.getDefault().unregister(this); + } + + private void initDrawer() { + navigationAdapter = new NavigationAdapter(this, () -> { + showPayPalForm(); + drawerLayout.closeDrawers(); + }); + navigationAdapter.setOnItemClickListener((item, view, position) -> { + if (item != NavigationElement.SEPARATOR) { + selectItem(item); + } + }); + navigationAdapter.selectItem(NavigationElement.LIBRARY); + rvNavigation.setLayoutManager(new LinearLayoutManager(this)); + rvNavigation.setAdapter(navigationAdapter); + + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.open, R.string.close) { + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + } + + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + } + }; + drawerLayout.addDrawerListener(drawerToggle); + + selectItem(NavigationElement.LIBRARY); + } + + public void selectItem(NavigationElement navigationElement) { + if (NavigationElement.PREMIUM == navigationElement) { + getPresenter().checkForPremiumBook(); + } else { + currentNavigationElement = navigationElement; + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction().replace(R.id.content, navigationElement.getFragment()).commit(); + setTitle(navigationElement.getTitle()); + } + drawerLayout.closeDrawers(); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + drawerToggle.syncState(); + } + + @Override + public void onBackPressed() { + if (NavigationElement.LIBRARY != currentNavigationElement) { + selectItem(NavigationElement.LIBRARY); + } else { + super.onBackPressed(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return drawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); + } + + @Override + protected void onHomeClicked() { + drawerLayout.openDrawer(GravityCompat.START); + } + + @SuppressWarnings("unused") + @Subscribe + public void onChangeNavigation(ChangeNavigationEvent event) { + selectItem(event.getElement()); + navigationAdapter.selectItem(event.getElement()); + } + + @OnClick(R.id.btnLogin) + public void onLoginClicked() { + getPresenter().onLoginClicked(); + } + + @OnClick(R.id.btnLogout) + public void onLogoutClicked() { + getPresenter().onLogoutClicked(); + } + + @Override + public void setLoggedIn(boolean loggedIn) { + if (loggedIn) { + btnLogin.setVisibility(View.GONE); + llLoggedInContainer.setVisibility(View.VISIBLE); + } else { + btnLogin.setVisibility(View.VISIBLE); + llLoggedInContainer.setVisibility(View.GONE); + } + } + + @Override + public void setLoggedUsername(String username) { + tvUsername.setText(username); + } + + @Override + public void setProgressDialogVisibility(boolean visible) { + if (visible && progressDialog == null) { + String dialogMessage = getString(R.string.main_view_progress); + progressDialog = ProgressDialog.show(this, null, dialogMessage, true, false); + } else if (!visible && progressDialog != null) { + progressDialog.hide(); + progressDialog = null; + } + } + + @Override + public void showToastMessage(int messageResId) { + Toast.makeText(this, messageResId, Toast.LENGTH_LONG).show(); + } + + @Override + public void showCustomTabsAuthentication(Uri authorizationUrl) { + showBrowserView(authorizationUrl); + } + + @Override + public void showPremiumBook(String slug) { + startActivity(new BookActivity.BookIntent(slug, BookType.TYPE_PREMIUM, this)); + } + + @Override + public void showNoPremiumBookAvailable(boolean userLoggedIn) { + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setTitle(R.string.no_prapremiere_title); + + if (!userLoggedIn) { + builder.setMessage(R.string.no_prapremiere_message) + .setPositiveButton(R.string.become_a_friend, (dialog, which) -> getPresenter().onBecomeAFriendClick()) + .setNegativeButton(R.string.no_thanks, (dialog, which) -> { + // nop. + }); + } else { + builder.setMessage(R.string.no_prapremiere_message_logged) + .setPositiveButton(R.string.OK, (dialog, which) -> { + // nop. + }); + } + builder.create() + .show(); + } + + @Override + public void showPremiumForm() { + showPayPalForm(); + } + + @Override + public void showLoginFirst() { + new AlertDialog.Builder(this) + .setTitle(R.string.login) + .setMessage(R.string.login_first) + .setPositiveButton(R.string.login, (dialog, which) -> { + getPresenter().onLoginClicked(); + }) + .setNegativeButton(R.string.no_thanks, (dialog, which) -> { + // nop. + }) + .create() + .show(); + } + + @Override + public void relaunch(int relaunchMessageResId) { + MainIntent intent = new MainIntent(relaunchMessageResId, this); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainPresenter.java new file mode 100644 index 0000000..0e0c452 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainPresenter.java @@ -0,0 +1,259 @@ +package com.moiseum.wolnelektury.view.main; + +import android.net.Uri; +import android.util.Log; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.base.mvp.Presenter; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.RestClientCallback; +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.connection.models.OAuthTokenModel; +import com.moiseum.wolnelektury.connection.models.UserModel; +import com.moiseum.wolnelektury.connection.services.BooksService; +import com.moiseum.wolnelektury.connection.services.UserService; +import com.moiseum.wolnelektury.events.LoggedInEvent; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; +import com.moiseum.wolnelektury.view.main.events.PremiumStatusEvent; + +import org.greenrobot.eventbus.EventBus; + +import java.util.List; + +import retrofit2.Call; + +/** + * Created by Piotr Ostrowski on 12.06.2018. + */ +class MainPresenter extends Presenter { + + private static final String TAG = MainPresenter.class.getSimpleName(); + private static final String OAUTH_CALLBACK_VALUE = "wolnelekturyapp://oauth.callback/?oauth_token="; + private static final String PAYPAL_SUCCESS_CALLBACK_VALUE = "wolnelekturyapp://paypal_return"; + private static final String PAYPAL_ERROR_CALLBACK_VALUE = "wolnelekturyapp://paypal_error"; + + private RestClient client = WLApplication.getInstance().getRestClient(); + private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + private Call currentCall; + private Call checkCall; + + MainPresenter(MainView view) { + super(view); + getView().setLoggedIn(preferences.isUserLoggedIn()); + getView().setLoggedUsername(preferences.getUsername()); + } + + @Override + public void onResume() { + super.onResume(); + if (preferences.isUserLoggedIn()) { + checkPremiumStatus(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (currentCall != null) { + currentCall.cancel(); + } + if (checkCall != null) { + checkCall.cancel(); + } + } + + void onLoginClicked() { + getView().setProgressDialogVisibility(true); + currentCall = client.call(new RestClientCallback() { + + @Override + public void onSuccess(OAuthTokenModel data) { + preferences.setTemporaryLoginToken(data.getToken()); + String authUrl = String.format(RestClient.WEB_OAUTH_AUTHORIZATION_URL, data.getToken()); + getView().setProgressDialogVisibility(false); + getView().showCustomTabsAuthentication(Uri.parse(authUrl)); + } + + @Override + public void onFailure(Exception e) { + Log.e(TAG, "Failed to obtain request token.", e); + getView().setProgressDialogVisibility(false); + getView().showToastMessage(R.string.login_request_token_failed); + } + + @Override + public void onCancel() { + getView().setProgressDialogVisibility(false); + } + + @Override + public Call execute(UserService service) { + return service.requestToken(); + } + }, UserService.class); + } + + void onLogoutClicked() { + client.clearOAuthTokens(); + preferences.clearUserData(); + getView().relaunch(R.string.logout_successful); + } + + void onBecomeAFriendClick() { + if (preferences.isUserLoggedIn()) { + getView().showPremiumForm(); + } else { + getView().showLoginFirst(); + } + } + + void onBrowserCallback(String intentData) { + if (PAYPAL_SUCCESS_CALLBACK_VALUE.equals(intentData)) { + preferences.setPremium(true); + EventBus.getDefault().post(new PremiumStatusEvent(true)); + getView().showToastMessage(R.string.premium_purchase_succeeded); + } else if (PAYPAL_ERROR_CALLBACK_VALUE.equals(intentData)) { + getView().showToastMessage(R.string.premium_purchase_failed); + } else { + onAuthorizationIntent(intentData); + } + } + + void checkForPremiumBook() { + fetchHeader(); + } + + private void onAuthorizationIntent(String intentData) { + String correctDataString = OAUTH_CALLBACK_VALUE + preferences.getTemporaryLoginToken(); + if (intentData.compareTo(correctDataString) != 0) { + getView().showToastMessage(R.string.login_auth_callback_malformed); + return; + } + + getView().setProgressDialogVisibility(true); + currentCall = client.call(new RestClientCallback() { + + @Override + public void onSuccess(OAuthTokenModel data) { + preferences.storeAccessToken(data); + fetchUser(); + } + + @Override + public void onFailure(Exception e) { + Log.e(TAG, "Failed to obtain access token.", e); + getView().setProgressDialogVisibility(false); + getView().showToastMessage(R.string.login_access_token_failed); + } + + @Override + public void onCancel() { + getView().setProgressDialogVisibility(false); + } + + @Override + public Call execute(UserService service) { + return service.accessToken(); + } + }, UserService.class); + } + + private void fetchUser() { + getView().setProgressDialogVisibility(true); + + /* + * We are marking user as logged in cause we already have credentials at this point. + * If this request fails we can perform it later on and ignore those errors for now. + */ + currentCall = client.call(new RestClientCallback() { + @Override + public void onSuccess(UserModel userModel) { + preferences.setUsername(userModel.getUsername()); + preferences.setPremium(userModel.isPremium()); + EventBus.getDefault().post(new LoggedInEvent()); + getView().setProgressDialogVisibility(false); + getView().setLoggedIn(true); + getView().setLoggedUsername(userModel.getUsername()); + } + + @Override + public void onFailure(Exception e) { + EventBus.getDefault().post(new LoggedInEvent()); + getView().setProgressDialogVisibility(false); + getView().setLoggedIn(true); + } + + @Override + public void onCancel() { + EventBus.getDefault().post(new LoggedInEvent()); + getView().setProgressDialogVisibility(false); + getView().setLoggedIn(true); + } + + @Override + public Call execute(UserService service) { + return service.getUser(); + } + }, UserService.class); + } + + private void fetchHeader() { + getView().setProgressDialogVisibility(true); + currentCall = client.call(new RestClientCallback, BooksService>() { + @Override + public void onSuccess(List data) { + getView().setProgressDialogVisibility(false); + if (data.size() > 0) { + getView().showPremiumBook(data.get(0).getSlug()); + } else { + getView().showNoPremiumBookAvailable(preferences.isUserLoggedIn()); + } + } + + @Override + public void onFailure(Exception e) { + getView().showToastMessage(R.string.fetching_premium_failed); + } + + @Override + public void onCancel() { + // nop. + } + + @Override + public Call> execute(BooksService service) { + return service.getPreview(); + } + }, BooksService.class); + } + + private void checkPremiumStatus() { + checkCall = client.call(new RestClientCallback() { + @Override + public void onSuccess(UserModel userModel) { + boolean currentPremiumStatus = preferences.isUserPremium(); + preferences.setUsername(userModel.getUsername()); + preferences.setPremium(userModel.isPremium()); + if (currentPremiumStatus && !userModel.isPremium()) { + getView().relaunch(R.string.subscription_lost); + } + } + + @Override + public void onFailure(Exception e) { + // nop + } + + @Override + public void onCancel() { + // nop + } + + @Override + public Call execute(UserService service) { + return service.getUser(); + } + }, UserService.class); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainView.java new file mode 100644 index 0000000..aa764de --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainView.java @@ -0,0 +1,30 @@ +package com.moiseum.wolnelektury.view.main; + +import android.net.Uri; +import android.support.annotation.StringRes; + +/** + * Created by Piotr Ostrowski on 12.06.2018. + */ +public interface MainView { + + void setLoggedIn(boolean loggedIn); + + void setLoggedUsername(String username); + + void setProgressDialogVisibility(boolean visible); + + void showToastMessage(@StringRes int messageResId); + + void showCustomTabsAuthentication(Uri authorizationUrl); + + void showPremiumBook(String slug); + + void showNoPremiumBookAvailable(boolean userLoggedIn); + + void showPremiumForm(); + + void showLoginFirst(); + + void relaunch(@StringRes int relaunchMessageResId); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationAdapter.java new file mode 100644 index 0000000..58f0597 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationAdapter.java @@ -0,0 +1,82 @@ +package com.moiseum.wolnelektury.view.main; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter; +import com.moiseum.wolnelektury.components.recycler.ViewHolder; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; + +/** + * @author golonkos + */ + +public class NavigationAdapter extends RecyclerAdapter> { + + private static int TYPE_ITEM = 0; + private static int TYPE_SEPARATOR = 1; + private static int TYPE_SUPPORT = 2; + private static int TYPE_BLANK = 3; + + private final SupportUsListener supportUsListener; + + private final SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + + NavigationAdapter(Context context, SupportUsListener supportUsListener) { + super(context, Selection.SINGLE); + this.supportUsListener = supportUsListener; + setItems(NavigationElement.valuesForNavigation()); + } + + @Override + public int getItemViewType(int position) { + NavigationElement element = getItem(position); + if (element.requiresLogin() && !preferences.isUserLoggedIn()) { + return TYPE_BLANK; + } + + switch (element) { + case SEPARATOR: + return TYPE_SEPARATOR; + case SUPPORT_US: + return TYPE_SUPPORT; + default: + return TYPE_ITEM; + } + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == TYPE_SEPARATOR) { + return new SeparatorViewHolder(inflate(R.layout.navigation_separator_item, parent)); + } + if (viewType == TYPE_SUPPORT) { + return new SupportViewHolder(inflate(R.layout.navigation_support_item, parent), supportUsListener); + } + if (viewType == TYPE_BLANK) { + return new NavigationBlankViewHolder(inflate(R.layout.navigation_blank, parent)); + } + return new NavigationViewHolder(inflate(R.layout.navigation_item, parent)); + } + + @Override + protected String getItemId(NavigationElement item) { + return item.name(); + } + + @Override + protected void onItemClicked(View view, NavigationElement item, int position) { + if (item != NavigationElement.SEPARATOR && item != NavigationElement.SUPPORT_US) { + super.onItemClicked(view, item, position); + } + } + + interface SupportUsListener { + void onSupportUsClicked(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationBlankViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationBlankViewHolder.java new file mode 100644 index 0000000..fbc2399 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationBlankViewHolder.java @@ -0,0 +1,20 @@ +package com.moiseum.wolnelektury.view.main; + +import android.view.View; + +import com.moiseum.wolnelektury.components.recycler.ViewHolder; + +/** + * Created by Piotr Ostrowski on 02.07.2018. + */ +public class NavigationBlankViewHolder extends ViewHolder { + + NavigationBlankViewHolder(View view) { + super(view); + } + + @Override + public void bind(NavigationElement item, boolean selected) { + // nop. + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationElement.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationElement.java new file mode 100644 index 0000000..219a154 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationElement.java @@ -0,0 +1,315 @@ +package com.moiseum.wolnelektury.view.main; + +import android.support.v4.app.Fragment; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.view.AboutFragment; +import com.moiseum.wolnelektury.view.book.list.BookListType; +import com.moiseum.wolnelektury.view.book.list.BooksListFragment; +import com.moiseum.wolnelektury.view.library.LibraryFragment; +import com.moiseum.wolnelektury.view.news.NewsListFragment; +import com.moiseum.wolnelektury.view.search.BookSearchFragment; +import com.moiseum.wolnelektury.view.settings.SettingsFragment; + +import java.util.Arrays; +import java.util.List; + +/** + * @author golonkos + */ + +public enum NavigationElement { + + LIBRARY { + @Override + public int getTitle() { + return R.string.nav_wolne_lektury; + } + + @Override + public Fragment getFragment() { + return LibraryFragment.newInstance(); + } + + @Override + public int getIcon() { + return R.drawable.ic_menu_library; + } + + @Override + public boolean requiresLogin() { + return false; + } + }, + + PREMIUM { + @Override + public int getTitle() { + return R.string.nav_premium; + } + + @Override + public Fragment getFragment() { + // This in intentional. We have to handle this separately. + return null; + } + + @Override + public int getIcon() { + return R.drawable.ic_menu_star; + } + + @Override + public boolean requiresLogin() { + return false; + } + }, + + NOW_READING { + @Override + public int getTitle() { + return R.string.nav_reading; + } + + @Override + public Fragment getFragment() { + return BooksListFragment.newInstance(BookListType.READING); + } + + @Override + public int getIcon() { + return R.drawable.ic_book; + } + + @Override + public boolean requiresLogin() { + return true; + } + }, + + FAVOURITES { + @Override + public int getTitle() { + return R.string.nav_favourites; + } + + @Override + public Fragment getFragment() { + return BooksListFragment.newInstance(BookListType.FAVOURITES); + } + + @Override + public int getIcon() { + return R.drawable.ic_menu_fav; + } + + @Override + public boolean requiresLogin() { + return true; + } + }, + + COMPLETED { + @Override + public int getTitle() { + return R.string.nav_completed; + } + + @Override + public Fragment getFragment() { + return BooksListFragment.newInstance(BookListType.COMPLETED); + } + + @Override + public int getIcon() { + return R.drawable.ic_accept; + } + + @Override + public boolean requiresLogin() { + return true; + } + }, + + AUDIOBOOKS { + @Override + public int getTitle() { + return R.string.nav_audiobooks; + } + + @Override + public Fragment getFragment() { + return BooksListFragment.newInstance(BookListType.AUDIOBOOKS); + } + + @Override + public int getIcon() { + return R.drawable.ic_menu_audiobook; + } + + @Override + public boolean requiresLogin() { + return false; + } + }, + + DOWNLOADED { + @Override + public int getTitle() { + return R.string.nav_my_collection; + } + + @Override + public Fragment getFragment() { + return BooksListFragment.newInstance(BookListType.DOWNLOADED); + } + + @Override + public int getIcon() { + return R.drawable.ic_menu_downloaded; + } + + @Override + public boolean requiresLogin() { + return false; + } + }, + + SEARCH { + @Override + public int getTitle() { + return R.string.nav_catalog; + } + + @Override + public Fragment getFragment() { + return BookSearchFragment.newInstance(); + } + + @Override + public int getIcon() { + return R.drawable.ic_menu_search; + } + + @Override + public boolean requiresLogin() { + return false; + } + }, + + ABOUT { + @Override + public int getTitle() { + return R.string.nav_about; + } + + @Override + public Fragment getFragment() { + return AboutFragment.newInstance(); + } + + @Override + public int getIcon() { + return R.drawable.ic_about; + } + + @Override + public boolean requiresLogin() { + return false; + } + }, + + NEWS { + @Override + public int getTitle() { + return R.string.nav_news; + } + + @Override + public Fragment getFragment() { + return NewsListFragment.newInstance(); + } + + @Override + public int getIcon() { + return R.drawable.ic_news; + } + + @Override + public boolean requiresLogin() { + return false; + } + }, + + SETTINGS { + @Override + public int getTitle(){return R.string.settings;} + + @Override + public Fragment getFragment() {return SettingsFragment.newInstance();} + + @Override + public int getIcon(){return R.drawable.ic_settings;} + + @Override + public boolean requiresLogin() { + return false; + } + }, + + SUPPORT_US { + @Override + public int getTitle() { + return -1; + } + + @Override + public Fragment getFragment() { + return null; + } + + @Override + public int getIcon() { + return -1; + } + + @Override + public boolean requiresLogin() { + return false; + } + }, + + SEPARATOR { + @Override + public int getTitle() { + return -1; + } + + @Override + public Fragment getFragment() { + return null; + } + + @Override + public int getIcon() { + return -1; + } + + @Override + public boolean requiresLogin() { + return false; + } + }; + + public abstract int getTitle(); + + public abstract Fragment getFragment(); + + public abstract int getIcon(); + + public abstract boolean requiresLogin(); + + public static List valuesForNavigation() { + return Arrays.asList(SUPPORT_US, LIBRARY, SEPARATOR, PREMIUM, SEARCH, AUDIOBOOKS, NOW_READING, FAVOURITES, COMPLETED, SEPARATOR, DOWNLOADED, SEPARATOR, NEWS, SETTINGS, ABOUT); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationViewHolder.java new file mode 100644 index 0000000..28e23e4 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationViewHolder.java @@ -0,0 +1,45 @@ +package com.moiseum.wolnelektury.view.main; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.support.v4.content.ContextCompat; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.components.recycler.ViewHolder; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; + +import butterknife.BindView; + +/** + * @author golonkos + */ + +public class NavigationViewHolder extends ViewHolder { + + @BindView(R.id.tvNavName) + TextView tvName; + @BindView(R.id.ivNavIcon) + ImageView ivNavIcon; + + NavigationViewHolder(View view) { + super(view); + } + + @Override + public void bind(NavigationElement item, boolean selected) { + tvName.setText(item.getTitle()); + ivNavIcon.setImageResource(item.getIcon()); + int color = selected ? R.color.white : R.color.turquoise; + ivNavIcon.setColorFilter(ContextCompat.getColor(getContext(), color), PorterDuff.Mode.SRC_IN); + if (item.getTitle() == R.string.nav_premium) { + tvName.setTextColor(getContext().getResources().getColor(R.color.orange_light)); + ivNavIcon.setColorFilter(ContextCompat.getColor(getContext(), R.color.orange_light), PorterDuff.Mode.SRC_IN); + } + + } + +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SeparatorViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SeparatorViewHolder.java new file mode 100644 index 0000000..546ae15 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SeparatorViewHolder.java @@ -0,0 +1,21 @@ +package com.moiseum.wolnelektury.view.main; + +import android.view.View; + +import com.moiseum.wolnelektury.components.recycler.ViewHolder; + +/** + * @author golonkos. + */ + +public class SeparatorViewHolder extends ViewHolder { + + public SeparatorViewHolder(View view) { + super(view); + } + + @Override + public void bind(NavigationElement item, boolean selected) { + + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SupportViewHolder.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SupportViewHolder.java new file mode 100644 index 0000000..39855e0 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SupportViewHolder.java @@ -0,0 +1,35 @@ +package com.moiseum.wolnelektury.view.main; + +import android.view.View; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.components.recycler.ViewHolder; + +import butterknife.OnClick; + +/** + * @author golonkos + */ + +public class SupportViewHolder extends ViewHolder { + + private final NavigationAdapter.SupportUsListener supportUsListener; + + public SupportViewHolder(View view, NavigationAdapter.SupportUsListener supportUsListener) { + super(view); + this.supportUsListener = supportUsListener; + } + + @Override + public void bind(NavigationElement item, boolean selected) { + //nop + } + + @SuppressWarnings("unused") + @OnClick(R.id.btnSupportUs) + public void onSupportUsClicked() { + if (supportUsListener != null) { + supportUsListener.onSupportUsClicked(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/events/PremiumStatusEvent.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/events/PremiumStatusEvent.java new file mode 100644 index 0000000..2d4a290 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/main/events/PremiumStatusEvent.java @@ -0,0 +1,16 @@ +package com.moiseum.wolnelektury.view.main.events; + +/** + * Created by Piotr Ostrowski on 26.08.2018. + */ +public class PremiumStatusEvent { + private boolean premium; + + public PremiumStatusEvent(boolean premium) { + this.premium = premium; + } + + public boolean isPremium() { + return premium; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListAdapter.java new file mode 100644 index 0000000..8e603a2 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListAdapter.java @@ -0,0 +1,57 @@ +package com.moiseum.wolnelektury.view.news; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter; +import com.moiseum.wolnelektury.components.recycler.ViewHolder; +import com.moiseum.wolnelektury.connection.models.NewsModel; + +import butterknife.BindView; + +public class NewsListAdapter extends RecyclerAdapter { + + static class NewsViewHolder extends ViewHolder { + + @BindView(R.id.textViewDate) + TextView txtDate; + @BindView(R.id.textViewLead) + TextView txtLead; + @BindView(R.id.ivNewsThumb) + ImageView newsImage; + + NewsViewHolder(View view) { + super(view); + } + + @Override + public void bind(NewsModel item, boolean selected) { + txtDate.setText(item.getTime()); + txtLead.setText(item.getTitle()); + Glide.with(getContext()).load(item.getThumbUrl()).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(newsImage); + } + } + + NewsListAdapter(Context context) { + super(context, RecyclerAdapter.Selection.NONE); + } + + @NonNull + @Override + public NewsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = inflate(R.layout.news_item, parent); + return new NewsViewHolder(view); + } + + @Override + protected String getItemId(NewsModel item) { + return item.getKey(); + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListFragment.java new file mode 100644 index 0000000..f10688c --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListFragment.java @@ -0,0 +1,105 @@ +package com.moiseum.wolnelektury.view.news; + +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.Toast; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.components.ProgressRecyclerView; +import com.moiseum.wolnelektury.components.recycler.EndlessRecyclerOnScrollListener; +import com.moiseum.wolnelektury.connection.models.NewsModel; +import com.moiseum.wolnelektury.view.news.single.NewsActivity; + +import java.util.List; + +import butterknife.BindView; +import butterknife.OnClick; + +public class NewsListFragment extends PresenterFragment implements NewsListView { + + public static NewsListFragment newInstance() { + return new NewsListFragment(); + } + + @BindView(R.id.rvNews) + ProgressRecyclerView rvNews; + @BindView(R.id.pbLoadMore) + ProgressBar pbLoadMore; + @BindView(R.id.btnReloadMore) + Button btnReloadMore; + + private NewsListAdapter adapter; + private EndlessRecyclerOnScrollListener rvBooksScrollListener = new EndlessRecyclerOnScrollListener() { + @Override + public void onLoadMore() { + if (adapter.getItemCount() > 0) { + getPresenter().loadMoreNews(); + } + } + }; + + @Override + protected NewsListPresenter createPresenter() { + return new NewsListPresenter(this); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_news; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + rvNews.setLayoutManager(layoutManager); + rvNews.addOnScrollListener(rvBooksScrollListener); + + adapter = new NewsListAdapter(getContext()); + adapter.setOnItemClickListener((item, view1, position) -> getPresenter().onNewsClicked(item)); + rvNews.setAdapter(adapter); + } + + @Override + public void setData(List data) { + if (adapter.getItemCount() == 0) { + rvNews.setItems(data); + } else { + rvNews.addItems(data); + } + } + + @Override + public void setProgressVisible(boolean visible) { + if (adapter.getItemCount() == 0) { + rvNews.setProgressVisible(visible); + } else { + pbLoadMore.setVisibility(visible ? View.VISIBLE : View.GONE); + } + } + + @Override + public void showError(Exception e) { + Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show(); + if (adapter.getItemCount() != 0) { + btnReloadMore.setVisibility(View.VISIBLE); + } else { + rvNews.showRetryButton(() -> getPresenter().loadMoreNews()); + } + } + + @Override + public void launchNews(NewsModel news) { + startActivity(new NewsActivity.NewsIntent(news, getContext())); + } + + @OnClick(R.id.btnReloadMore) + public void onReloadMoreClicked() { + btnReloadMore.setVisibility(View.GONE); + pbLoadMore.setVisibility(View.VISIBLE); + getPresenter().loadMoreNews(); + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListPresenter.java new file mode 100644 index 0000000..4b87b96 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListPresenter.java @@ -0,0 +1,81 @@ +package com.moiseum.wolnelektury.view.news; + +import android.os.Bundle; + +import com.moiseum.wolnelektury.base.DataObserver; +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.models.NewsModel; +import com.moiseum.wolnelektury.connection.services.NewsService; + +import java.util.Collections; +import java.util.List; + +import retrofit2.Call; + +/** + * Created by Piotr Ostrowski on 06.08.2018. + */ +class NewsListPresenter extends FragmentPresenter { + + private class NewsListDataProvider extends DataProvider, NewsService> { + + @Override + protected Class getServiceClass() { + return NewsService.class; + } + + @Override + public Call> execute(NewsService service) { + return service.getNews(lastKeySlug, RestClient.PAGINATION_LIMIT); + } + } + + private class NewsListDataObserver implements DataObserver> { + + @Override + public void onLoadStarted() { + getView().setProgressVisible(true); + } + + @Override + public void onLoadSuccess(List data) { + if (data.size() > 0) { + lastKey = data.get(data.size() - 1).getKey(); + } + getView().setProgressVisible(false); + getView().setData(data); + } + + @Override + public void onLoadFailed(Exception e) { + getView().setProgressVisible(false); + getView().setData(Collections.emptyList()); + getView().showError(e); + } + } + + private NewsListDataProvider dataProvider; + private String lastKey = null; + + NewsListPresenter(NewsListView view) { + super(view); + dataProvider = new NewsListDataProvider(); + dataProvider.setDataObserver(new NewsListDataObserver()); + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + dataProvider.load(null); + } + + public void loadMoreNews() { + dataProvider.load(lastKey); + } + + public void onNewsClicked(NewsModel newsModel) { + getView().launchNews(newsModel); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListView.java new file mode 100644 index 0000000..35bfc0b --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListView.java @@ -0,0 +1,14 @@ +package com.moiseum.wolnelektury.view.news; + +import com.moiseum.wolnelektury.base.mvp.LoadingView; +import com.moiseum.wolnelektury.connection.models.NewsModel; + +import java.util.List; + +/** + * Created by Piotr Ostrowski on 06.08.2018. + */ +public interface NewsListView extends LoadingView> { + + void launchNews(NewsModel news); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsActivity.java new file mode 100644 index 0000000..f5d4373 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsActivity.java @@ -0,0 +1,50 @@ +package com.moiseum.wolnelektury.view.news.single; + +import android.content.Context; +import android.os.Bundle; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.base.AbstractIntent; +import com.moiseum.wolnelektury.connection.models.NewsModel; + +import org.parceler.Parcels; + +import static com.moiseum.wolnelektury.view.news.single.NewsActivity.NewsIntent.NEWS_KEY; + +/** + * Created by Piotr Ostrowski on 06.08.2018. + */ +public class NewsActivity extends AbstractActivity { + + private static final String NEWS_FRAGMENT_TAG = "NewsFragmentTag"; + + public static class NewsIntent extends AbstractIntent { + static final String NEWS_KEY = "NewsKey"; + + public NewsIntent(NewsModel news, Context context) { + super(context, NewsActivity.class); + putExtra(NEWS_KEY, Parcels.wrap(news)); + } + } + + @Override + public int getLayoutResourceId() { + return R.layout.activity_blank; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + if (!getIntent().hasExtra(NEWS_KEY)) { + throw new IllegalStateException("Activity intent is missing news extras."); + } + setTitle(""); + + NewsModel news = Parcels.unwrap(getIntent().getParcelableExtra(NEWS_KEY)); + NewsFragment newsFragment = (NewsFragment) getSupportFragmentManager().findFragmentByTag(NEWS_FRAGMENT_TAG); + if (newsFragment == null) { + newsFragment = NewsFragment.newInstance(news); + getSupportFragmentManager().beginTransaction().add(R.id.flContainer, newsFragment, NEWS_FRAGMENT_TAG).commit(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsFragment.java new file mode 100644 index 0000000..cc2e108 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsFragment.java @@ -0,0 +1,129 @@ +package com.moiseum.wolnelektury.view.news.single; + +import android.os.Bundle; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.CollapsingToolbarLayout; +import android.support.v4.view.ViewPager; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.connection.models.NewsModel; +import com.moiseum.wolnelektury.view.news.zoom.ZoomActivity; + +import org.parceler.Parcels; +import org.sufficientlysecure.htmltextview.HtmlTextView; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.OnClick; +import me.relex.circleindicator.CircleIndicator; + +/** + * Created by Piotr Ostrowski on 06.08.2018. + */ +public class NewsFragment extends PresenterFragment implements NewsView { + + private static final String NEWS_ARGUMENT_KEY = "NewsArgumentKey"; + private static final int FIVE_PAGES = 5; + + public static NewsFragment newInstance(NewsModel news) { + NewsFragment fragment = new NewsFragment(); + Bundle args = new Bundle(1); + args.putParcelable(NEWS_ARGUMENT_KEY, Parcels.wrap(news)); + fragment.setArguments(args); + return fragment; + } + + @BindView(R.id.vpGallery) + ViewPager vpGallery; + @BindView(R.id.indicator) + CircleIndicator indicator; + @BindView(R.id.tvNewsTitle) + TextView tvNewsTitle; + @BindView(R.id.tvNewsTime) + TextView tvNewsTime; + @BindView(R.id.tvNewsPlace) + TextView tvNewsPlace; + @BindView(R.id.tvNewsBody) + HtmlTextView tvNewsBody; + @BindView(R.id.clMainView) + View clMainView; + @BindView(R.id.ctlCollapse) + CollapsingToolbarLayout ctlCollapse; + @BindView(R.id.llContentContainer) + LinearLayout llContentContainer; + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_single_news; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + Toolbar toolbar = view.findViewById(R.id.bookToolbar); + setupToolbar(toolbar); + } + + @Override + protected NewsPresenter createPresenter() { + if (getArguments() == null || getArguments().getParcelable(NEWS_ARGUMENT_KEY) == null) { + throw new IllegalStateException("Fragment is missing arguments"); + } + NewsModel news = Parcels.unwrap(getArguments().getParcelable(NEWS_ARGUMENT_KEY)); + return new NewsPresenter(news, this); + } + + @OnClick(R.id.fabShare) + public void onShareClick() { + getPresenter().onShareNewsClicked(); + } + + @Override + public void initializeNewsView(NewsModel news) { + ctlCollapse.setTitle(news.getTitle()); + ctlCollapse.setExpandedTitleColor(getResources().getColor(android.R.color.transparent)); + tvNewsTitle.setText(news.getTitle()); + tvNewsTime.setText(news.getTime()); + tvNewsPlace.setText(news.getPlace()); + tvNewsBody.setHtml(news.getBody()); + + NewsGalleryAdapter galleryAdapter = new NewsGalleryAdapter(news.getGalleryUrl(), getContext()); + vpGallery.setAdapter(galleryAdapter); + vpGallery.setOffscreenPageLimit(FIVE_PAGES); + indicator.setViewPager(vpGallery); + addDisposable(galleryAdapter.getPageClickObservable().subscribe(position -> { + ArrayList urls = new ArrayList<>(news.getGalleryUrl()); + startActivity(new ZoomActivity.ZoomIntent(urls, position, getContext())); + })); + enableToolbarCollapse(); + } + + @Override + public void startShareActivity(String shareUrl) { + showShareActivity(shareUrl); + } + + private void enableToolbarCollapse() { + ViewTreeObserver viewTreeObserver = llContentContainer.getViewTreeObserver(); + if (viewTreeObserver.isAlive()) { + viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + llContentContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); + if (ctlCollapse.getHeight() + llContentContainer.getHeight() > clMainView.getHeight()) { + AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) ctlCollapse.getLayoutParams(); + params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED); + ctlCollapse.setLayoutParams(params); + } + } + }); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsGalleryAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsGalleryAdapter.java new file mode 100644 index 0000000..a5dff6a --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsGalleryAdapter.java @@ -0,0 +1,78 @@ +package com.moiseum.wolnelektury.view.news.single; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.view.PagerAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.moiseum.wolnelektury.R; + +import java.util.List; + +import io.reactivex.Observable; +import io.reactivex.subjects.PublishSubject; + +/** + * Created by Piotr Ostrowski on 06.08.2018. + */ +public class NewsGalleryAdapter extends PagerAdapter { + + private final LayoutInflater inflater; + private final List galleryUrls; + + private PublishSubject pagerOnClickSubject = PublishSubject.create(); + private View.OnClickListener pageClickListener = v -> { + int position = (int) v.getTag(); + pagerOnClickSubject.onNext(position); + }; + + NewsGalleryAdapter(List galleryUrls, Context context) { + this.galleryUrls = galleryUrls; + this.inflater = LayoutInflater.from(context); + } + + @NonNull + @SuppressLint("InflateParams") + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + View view = inflater.inflate(R.layout.fragment_single_news_gallery_item, null); + view.setTag(position); + view.setOnClickListener(pageClickListener); + container.addView(view); + + ImageView ivGallery = view.findViewById(R.id.tvGalleryImage); + Glide.with(view.getContext()) + .load(galleryUrls.get(position)) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .dontTransform() + .into(ivGallery); + + return view; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + View view = (View) object; + container.removeView(view); + } + + @Override + public int getCount() { + return galleryUrls.size(); + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return view == object; + } + + Observable getPageClickObservable() { + return pagerOnClickSubject.hide(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsPresenter.java new file mode 100644 index 0000000..0b0b476 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsPresenter.java @@ -0,0 +1,39 @@ +package com.moiseum.wolnelektury.view.news.single; + +import android.os.Bundle; + +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.models.NewsModel; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Piotr Ostrowski on 06.08.2018. + */ +class NewsPresenter extends FragmentPresenter { + + private final NewsModel news; + + NewsPresenter(NewsModel news, NewsView view) { + super(view); + this.news = news; + + List galleryUrls = new ArrayList<>(news.getGalleryUrl().size() + 1); + if (news.getImageUrl() != null) { + galleryUrls.add(news.getImageUrl()); + } + galleryUrls.addAll(news.getGalleryUrl()); + this.news.setGalleryUrl(galleryUrls); + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + getView().initializeNewsView(news); + } + + void onShareNewsClicked() { + getView().startShareActivity(news.getUrl()); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsView.java new file mode 100644 index 0000000..d6180d5 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsView.java @@ -0,0 +1,11 @@ +package com.moiseum.wolnelektury.view.news.single; + +import com.moiseum.wolnelektury.connection.models.NewsModel; /** + * Created by Piotr Ostrowski on 06.08.2018. + */ +public interface NewsView { + + void initializeNewsView(NewsModel news); + + void startShareActivity(String shareUrl); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomActivity.java new file mode 100644 index 0000000..63c4f81 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomActivity.java @@ -0,0 +1,48 @@ +package com.moiseum.wolnelektury.view.news.zoom; + +import android.content.Context; +import android.os.Bundle; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.base.AbstractIntent; + +import java.util.ArrayList; + +/** + * Created by Piotr Ostrowski on 07.08.2018. + */ +public class ZoomActivity extends AbstractActivity { + + private static final String PHOTOS_URL_KEY = "PhotoUrls"; + private static final String POSITION_KEY = "PositionKey"; + private static final String ZOOM_FRAGMENT_TAG = "ZoomFragmentTag"; + + public static class ZoomIntent extends AbstractIntent { + public ZoomIntent(ArrayList photoUrls, int position, Context context) { + super(context, ZoomActivity.class); + putExtra(PHOTOS_URL_KEY, photoUrls); + putExtra(POSITION_KEY, position); + } + } + + @Override + public int getLayoutResourceId() { + return R.layout.activity_blank; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + if (!getIntent().hasExtra(PHOTOS_URL_KEY) || !getIntent().hasExtra(POSITION_KEY)) { + throw new IllegalStateException("Activity intent is missing news extras."); + } + + ArrayList photoUrls = getIntent().getStringArrayListExtra(PHOTOS_URL_KEY); + int position = getIntent().getIntExtra(POSITION_KEY, 0); + ZoomFragment zoomFragment = (ZoomFragment) getSupportFragmentManager().findFragmentByTag(ZOOM_FRAGMENT_TAG); + if (zoomFragment == null) { + zoomFragment = ZoomFragment.newInstance(photoUrls, position); + getSupportFragmentManager().beginTransaction().add(R.id.flContainer, zoomFragment, ZOOM_FRAGMENT_TAG).commit(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomFragment.java new file mode 100644 index 0000000..5962e4f --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomFragment.java @@ -0,0 +1,66 @@ +package com.moiseum.wolnelektury.view.news.zoom; + +import android.os.Bundle; +import android.support.v4.view.ViewPager; +import android.view.View; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import me.relex.circleindicator.CircleIndicator; + +/** + * Created by Piotr Ostrowski on 07.08.2018. + */ +public class ZoomFragment extends PresenterFragment implements ZoomView { + + private static final String PHOTOS_URL_KEY = "PhotoUrls"; + private static final String POSITION_KEY = "PositionKey"; + private static final int FIVE_PAGES = 5; + + @BindView(R.id.vpGallery) + ViewPager vpGallery; + @BindView(R.id.indicator) + CircleIndicator indicator; + + public static ZoomFragment newInstance(ArrayList photoUrls, int position) { + ZoomFragment fragment = new ZoomFragment(); + Bundle args = new Bundle(1); + args.putStringArrayList(PHOTOS_URL_KEY, photoUrls); + args.putInt(POSITION_KEY, position); + fragment.setArguments(args); + return fragment; + } + + @Override + protected ZoomPresenter createPresenter() { + if (getArguments() == null || getArguments().getStringArrayList(PHOTOS_URL_KEY) == null || getArguments().getInt(POSITION_KEY, -1) == -1) { + throw new IllegalStateException("Fragment is missing arguments"); + } + ArrayList urls = getArguments().getStringArrayList(PHOTOS_URL_KEY); + int position = getArguments().getInt(POSITION_KEY); + return new ZoomPresenter(urls, position, this); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_zoom; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + // nop. + } + + @Override + public void initializeZoomView(List photoUrls, int initialPosition) { + vpGallery.setAdapter(new ZoomPhotosAdapter(photoUrls, getContext())); + vpGallery.setOffscreenPageLimit(FIVE_PAGES); + vpGallery.setCurrentItem(initialPosition, false); + indicator.setViewPager(vpGallery); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPhotosAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPhotosAdapter.java new file mode 100644 index 0000000..839a91e --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPhotosAdapter.java @@ -0,0 +1,105 @@ +package com.moiseum.wolnelektury.view.news.zoom; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.PagerAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; +import com.moiseum.wolnelektury.R; + +import java.lang.ref.WeakReference; +import java.util.List; + +import it.sephiroth.android.library.imagezoom.ImageViewTouch; +import it.sephiroth.android.library.imagezoom.ImageViewTouchBase; + +/** + * Created by Piotr Ostrowski on 07.08.2018. + */ +public class ZoomPhotosAdapter extends PagerAdapter { + + private List photoUrls; + private LayoutInflater inflater; + private WeakReference mContext; + + ZoomPhotosAdapter(List photoUrl, Context context) { + inflater = LayoutInflater.from(context); + mContext = new WeakReference<>(context); + this.photoUrls = photoUrl; + for (String photo : photoUrl) { + Glide.with(context).load(photo).dontTransform().preload(); + } + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup collection, int position) { + final String photoUrl = photoUrls.get(position); + View view = inflater.inflate(R.layout.zoom_item, collection, false); + + final ProgressBar pbLoading = view.findViewById(R.id.pbLoading); + final TextView tvLoading = view.findViewById(R.id.tvLoading); + final Button btnRetry = view.findViewById(R.id.btnRetry); + final ImageViewTouch ivPhoto = view.findViewById(R.id.ivPointPhoto); + + btnRetry.setOnClickListener(v -> { + pbLoading.setVisibility(View.VISIBLE); + tvLoading.setVisibility(View.VISIBLE); + btnRetry.setVisibility(View.GONE); + fetchImageWithGlide(photoUrl, pbLoading, tvLoading, btnRetry, ivPhoto); + }); + + ivPhoto.setDisplayType(ImageViewTouchBase.DisplayType.FIT_HEIGHT); + fetchImageWithGlide(photoUrl, pbLoading, tvLoading, btnRetry, ivPhoto); + + collection.addView(view); + return view; + } + + @Override + public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) { + collection.removeView((View) view); + } + + @Override + public int getCount() { + return photoUrls.size(); + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return view == object; + } + + private void fetchImageWithGlide(String photoUrl, final ProgressBar pbLoading, final TextView tvLoading, final Button btnRetry, ImageViewTouch ivPhoto) { + if (mContext.get() != null) { + Glide.with(mContext.get()).load(photoUrl).dontTransform().listener(new RequestListener() { + @Override + public boolean onException(Exception e, String model, Target target, boolean isFirstResource) { + pbLoading.setVisibility(View.GONE); + tvLoading.setVisibility(View.GONE); + btnRetry.setVisibility(View.VISIBLE); + return true; + } + + @Override + public boolean onResourceReady(GlideDrawable resource, String model, Target target, boolean isFromMemoryCache, boolean isFirstResource) { + pbLoading.setVisibility(View.GONE); + tvLoading.setVisibility(View.GONE); + return false; + } + }).into(ivPhoto); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPresenter.java new file mode 100644 index 0000000..77ef263 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPresenter.java @@ -0,0 +1,28 @@ +package com.moiseum.wolnelektury.view.news.zoom; + +import android.os.Bundle; + +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; + +import java.util.List; + +/** + * Created by Piotr Ostrowski on 07.08.2018. + */ +public class ZoomPresenter extends FragmentPresenter { + + private final List photoUrls; + private final int initialPosition; + + ZoomPresenter(List photoUrls, int initialPosition, ZoomView view) { + super(view); + this.photoUrls = photoUrls; + this.initialPosition = initialPosition; + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + getView().initializeZoomView(photoUrls, initialPosition); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomView.java new file mode 100644 index 0000000..bef12ae --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomView.java @@ -0,0 +1,8 @@ +package com.moiseum.wolnelektury.view.news.zoom; + +import java.util.List; /** + * Created by Piotr Ostrowski on 07.08.2018. + */ +public interface ZoomView { + void initializeZoomView(List photoUrls, int initialPosition); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerActivity.java new file mode 100644 index 0000000..30aa6ce --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerActivity.java @@ -0,0 +1,48 @@ +package com.moiseum.wolnelektury.view.player; + +import android.content.Context; +import android.os.Bundle; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.base.AbstractIntent; +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; + +import org.parceler.Parcels; + +import static com.moiseum.wolnelektury.view.player.PlayerActivity.PlayerIntent.BOOK_KEY; +import static com.moiseum.wolnelektury.view.player.PlayerActivity.PlayerIntent.BOOK_SLUG_KEY; + +/** + * Created by Piotr Ostrowski on 22.05.2018. + */ +public class PlayerActivity extends AbstractActivity { + + private static final String PLAYER_FRAGMENT_TAG = "PlayerFragmentTag"; + + public static class PlayerIntent extends AbstractIntent { + + static final String BOOK_KEY = "BookKey"; + static final String BOOK_SLUG_KEY = "BookSlugKey"; + + public PlayerIntent(BookDetailsModel book, String slug, Context context) { + super(context, PlayerActivity.class); + putExtra(BOOK_KEY, Parcels.wrap(book)); + putExtra(BOOK_SLUG_KEY, slug); + } + } + + @Override + public int getLayoutResourceId() { + return R.layout.activity_blank; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + PlayerFragment playerFragment = (PlayerFragment) getSupportFragmentManager().findFragmentByTag(PLAYER_FRAGMENT_TAG); + if (playerFragment == null) { + playerFragment = PlayerFragment.newInstance(Parcels.unwrap(getIntent().getParcelableExtra(BOOK_KEY)), getIntent().getStringExtra(BOOK_SLUG_KEY)); + getSupportFragmentManager().beginTransaction().add(R.id.flContainer, playerFragment, PLAYER_FRAGMENT_TAG).commit(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerFragment.java new file mode 100644 index 0000000..ca92d2c --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerFragment.java @@ -0,0 +1,243 @@ +package com.moiseum.wolnelektury.view.player; + +import android.content.DialogInterface; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; +import android.view.View; +import android.widget.ImageButton; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.utils.StringUtils; +import com.moiseum.wolnelektury.view.player.header.PlayerHeaderFragment; +import com.moiseum.wolnelektury.view.player.playlist.PlayerPlaylistFragment; + +import org.parceler.Parcels; + +import butterknife.BindView; +import butterknife.OnClick; + +import static com.moiseum.wolnelektury.view.player.PlayerActivity.PlayerIntent.BOOK_KEY; +import static com.moiseum.wolnelektury.view.player.PlayerActivity.PlayerIntent.BOOK_SLUG_KEY; + + +/** + * Created by Piotr Ostrowski on 22.05.2018. + */ +public class PlayerFragment extends PresenterFragment implements PlayerView { + + private static final String TAG = PlayerFragment.class.getSimpleName(); + private AlertDialog errorDialog; + + public static PlayerFragment newInstance(BookDetailsModel book, String slug) { + PlayerFragment playerFragment = new PlayerFragment(); + Bundle args = new Bundle(); + args.putParcelable(BOOK_KEY, Parcels.wrap(book)); + args.putString(BOOK_SLUG_KEY, slug); + playerFragment.setArguments(args); + return playerFragment; + } + + private static final String HEADER_FRAGMENT_TAG = "HeaderFragmentTag"; + private static final String LIST_FRAGMENT_TAG = "ListFragmentTag"; + + int userSelectedPosition = 0; + private boolean mUserIsSeeking = false; + + private PlayerHeaderFragment headerFragment; + private PlayerPlaylistFragment playlistFragment; + + private SeekBar.OnSeekBarChangeListener listener = new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + tvCurrentProgress.setText(getPresenter().getCurrentTimerText(progress)); + if (fromUser) { + userSelectedPosition = progress; + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mUserIsSeeking = true; + getPresenter().playOrPause(true); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mUserIsSeeking = false; + getPresenter().seekTo(userSelectedPosition); + getPresenter().playOrPause(false); + } + }; + + @BindView(R.id.sbPlayerProgress) + SeekBar sbPlayerProgress; + @BindView(R.id.tvCurrentProgress) + TextView tvCurrentProgress; + @BindView(R.id.tvTotalProgress) + TextView tvTotalProgress; + @BindView(R.id.tvChapterTitle) + TextView tvChapterTitle; + @BindView(R.id.tvArtist) + TextView tvArtist; + @BindView(R.id.ibToggleList) + ImageButton ibToggleList; + @BindView(R.id.ibPlayPause) + ImageButton ibPlayPause; + @BindView(R.id.ibPrevious) + ImageButton ibPrevious; + @BindView(R.id.ibNext) + ImageButton ibNext; + + @Override + protected PlayerPresenter createPresenter() { + if (getArguments() == null || getArguments().getParcelable(BOOK_KEY) == null) { + throw new IllegalStateException("Book object is required at this point."); + } + return new PlayerPresenter( + Parcels.unwrap(getArguments().getParcelable(BOOK_KEY)), + getArguments().getString(BOOK_SLUG_KEY), + this, + getContext() + ); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_player; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + if (getArguments() == null || getArguments().getParcelable(BOOK_KEY) == null) { + throw new IllegalStateException("Book object is required at this point."); + } + BookDetailsModel book = Parcels.unwrap(getArguments().getParcelable(BOOK_KEY)); + initializeHeaderAndPlaylistFragments(book); + sbPlayerProgress.setOnSeekBarChangeListener(listener); + + int visibility = book.getAudiobookMediaModels().size() > 1 ? View.VISIBLE : View.GONE; + tvArtist.setVisibility(visibility); + ibToggleList.setVisibility(visibility); + ibPrevious.setVisibility(visibility); + ibNext.setVisibility(visibility); + } + + @OnClick(R.id.ibToggleList) + public void onToggleListClicked() { + FragmentManager fragmentManager = getFragmentManager(); + if (fragmentManager == null) { + return; + } + + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + if (playlistFragment.isHidden()) { + fragmentTransaction.show(playlistFragment); + fragmentTransaction.hide(headerFragment); + } else { + fragmentTransaction.hide(playlistFragment); + fragmentTransaction.show(headerFragment); + } + fragmentTransaction.commit(); + } + + @OnClick(R.id.ibPrevious) + public void onPreviousClicked() { + getPresenter().changeChapter(false); + } + + @OnClick(R.id.ibNext) + public void onNextClicked() { + getPresenter().changeChapter(true); + } + + @OnClick(R.id.ibRewind) + public void onRewindClicked() { + getPresenter().seekToButton(false); + } + + @OnClick(R.id.ibPlayPause) + public void onPauseClicked() { + getPresenter().playOrPause(false); + } + + @OnClick(R.id.ibFastForward) + public void onFastForwardClicked() { + getPresenter().seekToButton(true); + } + + @Override + public void setTrackDuration(int trackDuration, String totalProgress) { + sbPlayerProgress.setMax(trackDuration); + tvTotalProgress.setText(totalProgress); + } + + @Override + public void setTrackPosition(int position, String currentProgress) { + if (!mUserIsSeeking) { + sbPlayerProgress.setProgress(position); + tvCurrentProgress.setText(currentProgress); + } + } + + @Override + public void setTrackTexts(String title, int chapter) { + tvArtist.setText(getString(R.string.player_chapter_number, (chapter + 1))); + tvChapterTitle.setText(title); + } + + @Override + public void setPlayButtonState(boolean playing) { + if (getContext() != null) { + Drawable drawable = ContextCompat.getDrawable(getContext(), playing ? R.drawable.pause_selector : R.drawable.play_selector); + ibPlayPause.setImageDrawable(drawable); + } + } + + @Override + public void onPlayerError() { + if (getActivity() != null && errorDialog == null) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); + dialogBuilder.setMessage(getString(R.string.load_player_failed)); + dialogBuilder.setCancelable(false); + dialogBuilder.setPositiveButton(getString(R.string.close), (dialog, id) -> { + getActivity().finish(); + dialog.dismiss(); + }); + dialogBuilder.setOnDismissListener(dialog -> errorDialog = null); + errorDialog = dialogBuilder.create(); + errorDialog.show(); + } + } + + private void initializeHeaderAndPlaylistFragments(BookDetailsModel book) { + FragmentManager fragmentManager = getFragmentManager(); + if (fragmentManager == null) { + return; + } + + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + headerFragment = (PlayerHeaderFragment) fragmentManager.findFragmentByTag(HEADER_FRAGMENT_TAG); + if (headerFragment == null) { + headerFragment = PlayerHeaderFragment.newInstance(StringUtils.joinCategory(book.getAuthors(), ", "), book.getTitle(), book.getCoverThumb()); + fragmentTransaction.add(R.id.flPlayerFragmentContainer, headerFragment, HEADER_FRAGMENT_TAG); + } + playlistFragment = (PlayerPlaylistFragment) fragmentManager.findFragmentByTag(LIST_FRAGMENT_TAG); + if (playlistFragment == null) { + playlistFragment = PlayerPlaylistFragment.newInstance(book.getAudiobookMediaModels()); + fragmentTransaction.add(R.id.flPlayerFragmentContainer, playlistFragment, LIST_FRAGMENT_TAG); + } + fragmentTransaction.hide(playlistFragment); + fragmentTransaction.commit(); + } +} + + + diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerPresenter.java new file mode 100644 index 0000000..c409c75 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerPresenter.java @@ -0,0 +1,198 @@ +package com.moiseum.wolnelektury.view.player; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.storage.BookStorage; +import com.moiseum.wolnelektury.view.player.service.AudiobookLibrary; +import com.moiseum.wolnelektury.view.player.service.AudiobookService; +import com.moiseum.wolnelektury.view.player.service.MediaBrowserHelper; + +import java.util.List; +import java.util.Locale; + +/** + * Created by Piotr Ostrowski on 22.05.2018. + */ +public class PlayerPresenter extends FragmentPresenter { + + private final BookDetailsModel book; + private final BookModel storedBook; + + private final BookStorage storage = WLApplication.getInstance().getBookStorage(); + private MediaBrowserHelper mMediaBrowserHelper; + private boolean mIsPlaying; + + /** + * Customize the connection to our {@link android.support.v4.media.MediaBrowserServiceCompat} + * and implement our app specific desires. + */ + private class MediaBrowserConnection extends MediaBrowserHelper { + private MediaBrowserConnection(Context context) { + super(context, AudiobookService.class); + } + + @Override + protected void onChildrenLoaded(@NonNull String parentId, + @NonNull List children) { + super.onChildrenLoaded(parentId, children); + + final MediaControllerCompat mediaController = getMediaController(); + mediaController.getTransportControls().sendCustomAction(AudiobookService.ACTION_CLEAR_PLAYLIST, null); + + AudiobookLibrary.createAudiobookMetadata(book); + for (final MediaBrowserCompat.MediaItem mediaItem : AudiobookLibrary.getMediaItems()) { + mediaController.addQueueItem(mediaItem.getDescription()); + } + + // Call prepare now so pressing play just works. + mediaController.getTransportControls().prepare(); + if (storedBook != null) { + mediaController.getTransportControls().skipToQueueItem(storedBook.getCurrentAudioChapter()); + mediaController.getTransportControls().pause(); + } + } + } + + /** + * Implementation of the {@link MediaControllerCompat.Callback} methods we're interested in. + *

+ * Here would also be where one could override + * {@code onQueueChanged(List queue)} to get informed when items + * are added or removed from the queue. We don't do this here in order to keep the UI + * simple. + */ + private class PlayerMediaControllerCallback extends MediaControllerCompat.Callback { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat playbackState) { + if (playbackState != null) { + if (playbackState.getState() == PlaybackStateCompat.STATE_ERROR) { + getView().onPlayerError(); + } + + mIsPlaying = playbackState.getState() == PlaybackStateCompat.STATE_PLAYING; + getView().setPlayButtonState(mIsPlaying); + + if (mIsPlaying && playbackState.getExtras() != null) { + int total = playbackState.getExtras().getInt(AudiobookService.EXTRA_PLAYBACK_TOTAL); + getView().setTrackDuration(total, getCurrentTimerText(total)); + } + } + } + + @Override + public void onMetadataChanged(MediaMetadataCompat mediaMetadata) { + if (mediaMetadata == null) { + return; + } + if (!book.getAudiobookFilesUrls().contains(mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID))) { + return; + } + + int currentChapter = (int) mediaMetadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER); + if (storedBook != null) { + storedBook.setCurrentAudioChapter(currentChapter); + storage.update(storedBook); + } + + String chapterTitle = mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE); + getView().setTrackTexts(chapterTitle, currentChapter); + } + + @Override + public void onExtrasChanged(Bundle extras) { + if (extras.containsKey(AudiobookService.EXTRA_PLAYBACK_CURRENT)) { + int position = extras.getInt(AudiobookService.EXTRA_PLAYBACK_CURRENT); + getView().setTrackPosition(position, getCurrentTimerText(position)); + } + if (extras.containsKey(AudiobookService.EXTRA_PLAYBACK_TOTAL)) { + int total = extras.getInt(AudiobookService.EXTRA_PLAYBACK_TOTAL); + getView().setTrackDuration(total, getCurrentTimerText(total)); + } + } + + @Override + public void onSessionDestroyed() { + super.onSessionDestroyed(); + } + + @Override + public void onQueueChanged(List queue) { + super.onQueueChanged(queue); + } + } + + PlayerPresenter(BookDetailsModel book, String slug, PlayerView view, Context context) { + super(view); + this.book = book; + this.storedBook = storage.find(slug); + mMediaBrowserHelper = new MediaBrowserConnection(context); + mMediaBrowserHelper.registerCallback(new PlayerMediaControllerCallback()); + } + + @Override + public void onStart() { + super.onStart(); + mMediaBrowserHelper.onStart(); + } + + @Override + public void onStop() { + super.onStop(); + mMediaBrowserHelper.onStop(); + } + + public void playOrPause(boolean pauseCall) { + if (mIsPlaying) { + mMediaBrowserHelper.getTransportControls().pause(); + } else { + mMediaBrowserHelper.getTransportControls().play(); + } + } + + public void changeChapter(boolean next) { + if (next) { + mMediaBrowserHelper.getTransportControls().skipToNext(); + } else { + mMediaBrowserHelper.getTransportControls().skipToPrevious(); + } + } + + public void seekToButton(boolean forward) { + if (forward) { + mMediaBrowserHelper.getTransportControls().fastForward(); + } else { + mMediaBrowserHelper.getTransportControls().rewind(); + } + } + + public void seekTo(int userSelectedPosition) { + mMediaBrowserHelper.getTransportControls().seekTo(userSelectedPosition); + } + + public String getCurrentTimerText(int currentPosition) { + StringBuilder sb = new StringBuilder(); + int minutes = (currentPosition % (1000 * 60 * 60)) / (1000 * 60); + int seconds = ((currentPosition % (1000 * 60 * 60)) % (1000 * 60) / 1000); + if (currentPosition > (1000 * 60 * 60)) { + int hours = (currentPosition / (1000 * 60 * 60)); + sb.append(String.format(Locale.getDefault(), "%01d", hours)); + sb.append(":"); + } + sb.append(String.format(Locale.getDefault(), "%02d", minutes)); + sb.append(":"); + sb.append(String.format(Locale.getDefault(), "%02d", seconds)); + return sb.toString(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerView.java new file mode 100644 index 0000000..03f6a6d --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerView.java @@ -0,0 +1,17 @@ +package com.moiseum.wolnelektury.view.player; + +/** + * Created by Piotr Ostrowski on 22.05.2018. + */ +interface PlayerView { + + void setTrackDuration(int trackDuration, String totalProgress); + + void setTrackPosition(int position, String currentProgress); + + void setTrackTexts(String title, int chapter); + + void setPlayButtonState(boolean playing); + + void onPlayerError(); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderFragment.java new file mode 100644 index 0000000..12902fd --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderFragment.java @@ -0,0 +1,103 @@ +package com.moiseum.wolnelektury.view.player.header; + +import android.graphics.Color; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.connection.RestClient; + +import butterknife.BindView; +import butterknife.OnClick; +import retrofit2.http.HEAD; + +/** + * Created by Piotr Ostrowski on 22.05.2018. + */ +public class PlayerHeaderFragment extends PresenterFragment implements PlayerHeaderView { + + private static final String AUTHOR_KEY = "AuthorKey"; + private static final String TITLE_KEY = "TitleKey"; + private static final String COVER_KEY = "CoverKey"; + + public static PlayerHeaderFragment newInstance(String author, String title, String coverUrl) { + PlayerHeaderFragment fragment = new PlayerHeaderFragment(); + Bundle args = new Bundle(); + args.putString(AUTHOR_KEY, author); + args.putString(TITLE_KEY, title); + args.putString(COVER_KEY, coverUrl); + fragment.setArguments(args); + return fragment; + } + + + @BindView(R.id.vCoverOverlay) + View vCoverOverlay; + @BindView(R.id.ivCoverBackground) + ImageView ivCoverBackground; + @BindView(R.id.ivCover) + ImageView ivCover; + @BindView(R.id.tvAuthor) + TextView tvAuthor; + @BindView(R.id.tvBookTitle) + TextView tvBookTitle; + + @Override + protected PlayerHeaderPresenter createPresenter() { + if (getArguments() == null) { + throw new IllegalStateException("Missing fragment arguments."); + } + String author = getArguments().getString(AUTHOR_KEY); + String title = getArguments().getString(TITLE_KEY); + String coverUrl = getArguments().getString(COVER_KEY); + return new PlayerHeaderPresenter(author, title, coverUrl, this); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_player_header; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + initView(); + } + + public void initView() { + if (getArguments() != null) { + vCoverOverlay.setAlpha(0.7f); + vCoverOverlay.setBackgroundColor(Color.parseColor("#db4b16")); + tvAuthor.setText(getArguments().getString(AUTHOR_KEY)); + tvBookTitle.setText(getArguments().getString(TITLE_KEY)); + if (getArguments().getString(COVER_KEY) != null) { + String coverUrl = getArguments().getString(COVER_KEY); + if (coverUrl != null && !coverUrl.contains(RestClient.MEDIA_URL) && !coverUrl.contains(RestClient.MEDIA_URL_HTTPS)) { + coverUrl = RestClient.MEDIA_URL_HTTPS + coverUrl; + } + Glide.with(getContext()).load(coverUrl).placeholder(R.drawable.list_nocover).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivCover); + } + if (getArguments().getString(COVER_KEY) != null) { + String coverBackgroundUrl = getArguments().getString(COVER_KEY); + if (coverBackgroundUrl != null && !coverBackgroundUrl.contains(RestClient.MEDIA_URL) && !coverBackgroundUrl.contains(RestClient.MEDIA_URL_HTTPS)) { + coverBackgroundUrl = RestClient.MEDIA_URL_HTTPS + coverBackgroundUrl; + } + Glide.with(getContext()).load(coverBackgroundUrl).diskCacheStrategy(DiskCacheStrategy.ALL).dontTransform().into(ivCoverBackground); + } + } + } + + @OnClick(R.id.ibBack) + public void onBackButtonClicked() { + if (getActivity() != null) { + getActivity().finish(); + } + } +} + diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderPresenter.java new file mode 100644 index 0000000..4cf7a86 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderPresenter.java @@ -0,0 +1,13 @@ +package com.moiseum.wolnelektury.view.player.header; + +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; + +/** + * Created by Piotr Ostrowski on 22.05.2018. + */ +public class PlayerHeaderPresenter extends FragmentPresenter { + + public PlayerHeaderPresenter(String author, String title, String coverUrl, PlayerHeaderView view) { + super(view); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderView.java new file mode 100644 index 0000000..eed238b --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderView.java @@ -0,0 +1,7 @@ +package com.moiseum.wolnelektury.view.player.header; + +/** + * Created by Piotr Ostrowski on 22.05.2018. + */ +interface PlayerHeaderView { +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistAdapter.java new file mode 100644 index 0000000..91b38c8 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistAdapter.java @@ -0,0 +1,51 @@ +package com.moiseum.wolnelektury.view.player.playlist; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter; +import com.moiseum.wolnelektury.components.recycler.ViewHolder; +import com.moiseum.wolnelektury.connection.models.MediaModel; + +import butterknife.BindView; + +public class PlayerPlaylistAdapter extends RecyclerAdapter { + + PlayerPlaylistAdapter(Context context) { + super(context, Selection.SINGLE); + } + + @Override + protected String getItemId(MediaModel item) { + return item.getUrl(); + } + + @NonNull + @Override + public PlayerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new PlayerViewHolder(inflate(R.layout.playlist_item, parent)); + } + + static class PlayerViewHolder extends ViewHolder { + + @BindView(R.id.tvMediaName) + TextView tvMediaName; + @BindView(R.id.ibPlay) + ImageButton ibPlay; + + PlayerViewHolder(View view) { + super(view); + } + + @Override + public void bind(MediaModel item, boolean selected) { + tvMediaName.setText(item.getName()); + ibPlay.setVisibility(selected ? View.VISIBLE : View.INVISIBLE); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistFragment.java new file mode 100644 index 0000000..c1333db --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistFragment.java @@ -0,0 +1,72 @@ +package com.moiseum.wolnelektury.view.player.playlist; + +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.view.View; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.components.ProgressRecyclerView; +import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter; +import com.moiseum.wolnelektury.connection.models.MediaModel; + +import org.parceler.Parcels; + +import java.util.List; + +import butterknife.BindView; + +/** + * Created by Piotr Ostrowski on 28.05.2018. + */ +public class PlayerPlaylistFragment extends PresenterFragment implements PlayerPlaylistView { + + private static final String MEDIA_FILES_KEY = "MediaFilesKey"; + + @BindView(R.id.rvPlayerPlaylist) + ProgressRecyclerView rvPlayerPlaylist; + + private PlayerPlaylistAdapter adapter; + + public static PlayerPlaylistFragment newInstance(List mediaFiles) { + PlayerPlaylistFragment fragment = new PlayerPlaylistFragment(); + Bundle args = new Bundle(); + args.putParcelable(MEDIA_FILES_KEY, Parcels.wrap(mediaFiles)); + fragment.setArguments(args); + return fragment; + } + + @Override + protected PlayerPlaylistPresenter createPresenter() { + if (getArguments() == null || getArguments().getParcelable(MEDIA_FILES_KEY) == null) { + throw new IllegalStateException("Media files object is required at this point."); + } + return new PlayerPlaylistPresenter(Parcels.unwrap(getArguments().getParcelable(MEDIA_FILES_KEY)), this, getContext()); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_player_playlist; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + initList(rvPlayerPlaylist); + } + + @Override + public void setCurrentPlaylistItem(int position) { + adapter.selectItem(adapter.getItem(position)); + } + + public void initList(ProgressRecyclerView rvList) { + adapter = new PlayerPlaylistAdapter(getContext()); + adapter.setOnItemClickListener((item, view, position) -> getPresenter().onPlaylistItemClick(position)); + rvList.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); + rvList.setAdapter(adapter); + } + + public void setPlaylist(List item) { + rvPlayerPlaylist.setItems(item); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistPresenter.java new file mode 100644 index 0000000..6aa2eb7 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistPresenter.java @@ -0,0 +1,85 @@ +package com.moiseum.wolnelektury.view.player.playlist; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import com.moiseum.wolnelektury.base.DataProvider; +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.MediaModel; +import com.moiseum.wolnelektury.connection.services.BooksService; +import com.moiseum.wolnelektury.storage.BookStorage; +import com.moiseum.wolnelektury.view.book.list.BookListType; +import com.moiseum.wolnelektury.view.player.PlayerPresenter; +import com.moiseum.wolnelektury.view.player.service.AudiobookService; +import com.moiseum.wolnelektury.view.player.service.MediaBrowserHelper; + +import java.util.List; + +/** + * Created by Piotr Ostrowski on 28.05.2018. + */ +public class PlayerPlaylistPresenter extends FragmentPresenter { + + private class PlayerPlaylistMediaControllerCallback extends MediaControllerCompat.Callback { + + @Override + public void onMetadataChanged(MediaMetadataCompat mediaMetadata) { + if (mediaMetadata == null) { + return; + } + if (!containsMediaId(mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID))) { + return; + } + + int currentChapter = (int) mediaMetadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER); + getView().setCurrentPlaylistItem(currentChapter); + } + + private boolean containsMediaId(String mediaIdUrl) { + for (MediaModel model : media) { + if (model.getUrl().equals(mediaIdUrl)) { + return true; + } + } + return false; + } + } + + private final List media; + private MediaBrowserHelper mMediaBrowserHelper; + + PlayerPlaylistPresenter(List mediaFiles, PlayerPlaylistView view, Context context) { + super(view); + media = mediaFiles; + mMediaBrowserHelper = new MediaBrowserHelper(context, AudiobookService.class); + mMediaBrowserHelper.registerCallback(new PlayerPlaylistMediaControllerCallback()); + } + + @Override + public void onStart() { + super.onStart(); + mMediaBrowserHelper.onStart(); + } + + @Override + public void onStop() { + super.onStop(); + mMediaBrowserHelper.onStop(); + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + getView().setPlaylist(media); + } + + void onPlaylistItemClick(int itemPosition) { + mMediaBrowserHelper.getTransportControls().skipToQueueItem(itemPosition); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistView.java new file mode 100644 index 0000000..3a53403 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistView.java @@ -0,0 +1,15 @@ +package com.moiseum.wolnelektury.view.player.playlist; + +import com.moiseum.wolnelektury.connection.models.MediaModel; + +import java.util.List; + +/** + * Created by Piotr Ostrowski on 28.05.2018. + */ +public interface PlayerPlaylistView { + + void setPlaylist(List item); + + void setCurrentPlaylistItem(int position); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/AudiobookLibrary.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/AudiobookLibrary.java new file mode 100755 index 0000000..1fc9918 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/AudiobookLibrary.java @@ -0,0 +1,148 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * 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. + */ + +package com.moiseum.wolnelektury.view.player.service; + +import android.content.Context; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaMetadataCompat; + +import com.moiseum.wolnelektury.connection.models.BookDetailsModel; +import com.moiseum.wolnelektury.connection.models.MediaModel; +import com.moiseum.wolnelektury.utils.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.TreeMap; + + +public class AudiobookLibrary { + + private static final TreeMap music = new TreeMap<>(); +// private static final HashMap albumRes = new HashMap<>(); + private static final HashMap musicFileName = new HashMap<>(); + + public static String getRoot() { + return "root"; + } + +// private static String getAlbumArtUri(String albumArtResName) { +// return ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + +// BuildConfig.APPLICATION_ID + "/drawable/" + albumArtResName; +// } + + public static String getMusicFilename(String mediaId) { + return musicFileName.containsKey(mediaId) ? musicFileName.get(mediaId) : null; + } + +// private static int getAlbumRes(String mediaId) { +// return albumRes.containsKey(mediaId) ? albumRes.get(mediaId) : 0; +// } + +// public static Bitmap getAlbumBitmap(Context context, String mediaId) { +// return BitmapFactory.decodeResource(context.getResources(), +// AudiobookLibrary.getAlbumRes(mediaId)); +// } + + public static List getMediaItems() { + List result = new ArrayList<>(); + for (MediaMetadataCompat metadata : music.values()) { + result.add( + new MediaBrowserCompat.MediaItem( + metadata.getDescription(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)); + } + return result; + } + + public static MediaMetadataCompat getMetadata(Context context, String mediaId) { + MediaMetadataCompat metadataWithoutBitmap = music.get(mediaId); +// Bitmap albumArt = getAlbumBitmap(context, mediaId); + + // Since MediaMetadataCompat is immutable, we need to create a copy to set the album art. + // We don't set it initially on all items so that they don't take unnecessary memory. + MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); + for (String key : + new String[]{ + MediaMetadataCompat.METADATA_KEY_MEDIA_ID, + MediaMetadataCompat.METADATA_KEY_TITLE, + MediaMetadataCompat.METADATA_KEY_ARTIST, + MediaMetadataCompat.METADATA_KEY_ALBUM, + MediaMetadataCompat.METADATA_KEY_AUTHOR, + MediaMetadataCompat.METADATA_KEY_GENRE, + MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI + }) { + builder.putString(key, metadataWithoutBitmap.getString(key)); + } + builder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, metadataWithoutBitmap.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER)); + builder.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, metadataWithoutBitmap.getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS)); +// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt); + return builder.build(); + } + + public static void createAudiobookMetadata(BookDetailsModel book) { + music.clear(); + int index = 0; + List medias = book.getAudiobookMediaModels(); + + for (MediaModel model : medias) { + createMediaMetadataCompat( + model.getUrl(), + model.getName(), + model.getArtist(), + book.getTitle(), + StringUtils.joinCategory(book.getAuthors(), ", "), + StringUtils.joinCategory(book.getGenres(), ", "), + book.getCoverThumb(), + model.getUrl(), + index++, + medias.size() + ); + } + } + + private static void createMediaMetadataCompat( + String mediaId, + String title, + String artist, + String album, + String author, + String genre, + String artUrl, + String musicFilename, + int trackNumber, + int tracksCount + ) { + music.put( + mediaId, + new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId) + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album) + .putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, author) + .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, artUrl) +// .putString( +// MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, +// getAlbumArtUri(albumArtResName)) + .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber) + .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, tracksCount) + .build()); +// albumRes.put(mediaId, albumArtResId); + musicFileName.put(mediaId, musicFilename); + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/AudiobookService.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/AudiobookService.java new file mode 100755 index 0000000..a95f53f --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/AudiobookService.java @@ -0,0 +1,286 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * 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. + */ + +package com.moiseum.wolnelektury.view.player.service; + +import android.app.Notification; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaBrowserServiceCompat; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +public class AudiobookService extends MediaBrowserServiceCompat { + + private static final String TAG = AudiobookService.class.getSimpleName(); + + public static final String ACTION_CLEAR_PLAYLIST = "CommandClear"; + public static final String EXTRA_PLAYBACK_CURRENT = "PlaybackCurrent"; + public static final String EXTRA_PLAYBACK_TOTAL = "PlaybackTotal"; + + private MediaSessionCompat mSession; + private PlayerAdapter mPlayback; + private MediaNotificationManager mMediaNotificationManager; + private MediaSessionCallback mCallback; + private boolean mServiceInStartedState; + + @Override + public void onCreate() { + super.onCreate(); + + // Create a new MediaSession. + mSession = new MediaSessionCompat(this, "AudiobookService"); + mCallback = new MediaSessionCallback(); + mSession.setCallback(mCallback); + mSession.setFlags( + MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | + MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS | + MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + setSessionToken(mSession.getSessionToken()); + + mMediaNotificationManager = new MediaNotificationManager(this); + + mPlayback = new MediaPlayerAdapter(this, new MediaPlayerListener()); + Log.d(TAG, "onCreate: AudiobookService creating MediaSession, and MediaNotificationManager"); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + stopSelf(); + } + + @Override + public void onDestroy() { + mMediaNotificationManager.onDestroy(); + mPlayback.stop(); + mSession.release(); + Log.d(TAG, "onDestroy: MediaPlayerAdapter stopped, and MediaSession released"); + } + + @Override + public BrowserRoot onGetRoot(@NonNull String clientPackageName, + int clientUid, + Bundle rootHints) { + return new BrowserRoot(AudiobookLibrary.getRoot(), null); + } + + @Override + public void onLoadChildren( + @NonNull final String parentMediaId, + @NonNull final Result> result) { + result.sendResult(AudiobookLibrary.getMediaItems()); + } + + // MediaSession Callback: Transport Controls -> MediaPlayerAdapter + public class MediaSessionCallback extends MediaSessionCompat.Callback { + private final List mPlaylist = new ArrayList<>(); + private int mQueueIndex = -1; + private MediaMetadataCompat mPreparedMedia; + + @Override + public void onCustomAction(String action, Bundle extras) { + if (ACTION_CLEAR_PLAYLIST.equals(action)) { + mPlaylist.clear(); + mQueueIndex = 0; + mSession.setQueue(mPlaylist); + } + } + + @Override + public void onAddQueueItem(MediaDescriptionCompat description) { + mPlaylist.add(new MediaSessionCompat.QueueItem(description, description.hashCode())); + mQueueIndex = (mQueueIndex == -1) ? 0 : mQueueIndex; + mSession.setQueue(mPlaylist); + } + + @Override + public void onRemoveQueueItem(MediaDescriptionCompat description) { + mPlaylist.remove(new MediaSessionCompat.QueueItem(description, description.hashCode())); + mQueueIndex = (mPlaylist.isEmpty()) ? -1 : mQueueIndex; + mSession.setQueue(mPlaylist); + } + + @Override + public void onPrepare() { + if (mQueueIndex < 0 && mPlaylist.isEmpty()) { + // Nothing to play. + return; + } + + final String mediaId = mPlaylist.get(mQueueIndex).getDescription().getMediaId(); + mPreparedMedia = AudiobookLibrary.getMetadata(AudiobookService.this, mediaId); + mSession.setMetadata(mPreparedMedia); + + if (!mSession.isActive()) { + mSession.setActive(true); + } + } + + @Override + public void onPlay() { + if (!isReadyToPlay()) { + // Nothing to play. + return; + } + + if (mPreparedMedia == null) { + onPrepare(); + } + + mPlayback.playFromMedia(mPreparedMedia); + Log.d(TAG, "onPlayFromMediaId: MediaSession active"); + } + + @Override + public void onPause() { + mPlayback.pause(); + } + + @Override + public void onStop() { + mPlayback.stop(); + mSession.setActive(false); + } + + @Override + public void onSkipToNext() { + mQueueIndex = (++mQueueIndex % mPlaylist.size()); + mPreparedMedia = null; + onPlay(); + } + + @Override + public void onSkipToPrevious() { + mQueueIndex = mQueueIndex > 0 ? mQueueIndex - 1 : mPlaylist.size() - 1; + mPreparedMedia = null; + onPlay(); + } + + @Override + public void onSkipToQueueItem(long id) { + mQueueIndex = (int) id; + mPreparedMedia = null; + onPlay(); + } + + @Override + public void onSeekTo(long pos) { + mPlayback.seekTo(pos); + } + + @Override + public void onFastForward() { + mPlayback.fastForward(); + } + + @Override + public void onRewind() { + mPlayback.rewind(); + } + + private boolean isReadyToPlay() { + return (!mPlaylist.isEmpty()); + } + } + + // MediaPlayerAdapter Callback: MediaPlayerAdapter state -> AudiobookService. + public class MediaPlayerListener extends PlaybackInfoListener { + + private final ServiceManager mServiceManager; + + MediaPlayerListener() { + mServiceManager = new ServiceManager(); + } + + @Override + public void onPlaybackStateChange(PlaybackStateCompat state) { + // Report the state to the MediaSession. + mSession.setPlaybackState(state); + + // Manage the started state of this service. + switch (state.getState()) { + case PlaybackStateCompat.STATE_PLAYING: + mServiceManager.moveServiceToStartedState(state); + break; + case PlaybackStateCompat.STATE_PAUSED: + mServiceManager.updateNotificationForPause(state); + break; + case PlaybackStateCompat.STATE_STOPPED: + mServiceManager.moveServiceOutOfStartedState(state); + break; + } + } + + @Override + public void onPlaybackProgress(int current) { + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_PLAYBACK_CURRENT, current); + mSession.setExtras(bundle); + } + + @Override + public void onPlaybackPrepared(int duration) { + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_PLAYBACK_TOTAL, duration); + mSession.setExtras(bundle); + } + + class ServiceManager { + + private void moveServiceToStartedState(PlaybackStateCompat state) { + Notification notification = + mMediaNotificationManager.getNotification( + mPlayback.getCurrentMedia(), state, getSessionToken()); + + if (!mServiceInStartedState) { + ContextCompat.startForegroundService( + AudiobookService.this, + new Intent(AudiobookService.this, AudiobookService.class)); + mServiceInStartedState = true; + } + + startForeground(MediaNotificationManager.NOTIFICATION_ID, notification); + } + + private void updateNotificationForPause(PlaybackStateCompat state) { + stopForeground(false); + Notification notification = + mMediaNotificationManager.getNotification( + mPlayback.getCurrentMedia(), state, getSessionToken()); + mMediaNotificationManager.getNotificationManager() + .notify(MediaNotificationManager.NOTIFICATION_ID, notification); + } + + private void moveServiceOutOfStartedState(PlaybackStateCompat state) { + stopForeground(true); + stopSelf(); + mServiceInStartedState = false; + } + } + + } + +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaBrowserHelper.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaBrowserHelper.java new file mode 100755 index 0000000..413b84b --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaBrowserHelper.java @@ -0,0 +1,249 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * 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. + */ + +package com.moiseum.wolnelektury.view.player.service; + +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaBrowserServiceCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaControllerCompat.Callback; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for a MediaBrowser that handles connecting, disconnecting, + * and basic browsing with simplified callbacks. + */ +public class MediaBrowserHelper { + + private static final String TAG = MediaBrowserHelper.class.getSimpleName(); + + private final Context mContext; + private final Class mMediaBrowserServiceClass; + + private final List mCallbackList = new ArrayList<>(); + + private final MediaBrowserConnectionCallback mMediaBrowserConnectionCallback; + private final MediaControllerCallback mMediaControllerCallback; + private final MediaBrowserSubscriptionCallback mMediaBrowserSubscriptionCallback; + + private MediaBrowserCompat mMediaBrowser; + + @Nullable + private MediaControllerCompat mMediaController; + + public MediaBrowserHelper(Context context, + Class serviceClass) { + mContext = context; + mMediaBrowserServiceClass = serviceClass; + + mMediaBrowserConnectionCallback = new MediaBrowserConnectionCallback(); + mMediaControllerCallback = new MediaControllerCallback(); + mMediaBrowserSubscriptionCallback = new MediaBrowserSubscriptionCallback(); + } + + public void onStart() { + if (mMediaBrowser == null) { + mMediaBrowser = + new MediaBrowserCompat( + mContext, + new ComponentName(mContext, mMediaBrowserServiceClass), + mMediaBrowserConnectionCallback, + null); + mMediaBrowser.connect(); + } + Log.d(TAG, "onStart: Creating MediaBrowser, and connecting"); + } + + public void onStop() { + if (mMediaController != null) { + mMediaController.unregisterCallback(mMediaControllerCallback); + mMediaController = null; + } + if (mMediaBrowser != null && mMediaBrowser.isConnected()) { + mMediaBrowser.disconnect(); + mMediaBrowser = null; + } + resetState(); + Log.d(TAG, "onStop: Releasing MediaController, Disconnecting from MediaBrowser"); + } + + /** + * Called after connecting with a {@link MediaBrowserServiceCompat}. + *

+ * Override to perform processing after a connection is established. + * + * @param mediaController {@link MediaControllerCompat} associated with the connected + * MediaSession. + */ + protected void onConnected(@NonNull MediaControllerCompat mediaController) { + } + + /** + * Called after loading a browsable {@link MediaBrowserCompat.MediaItem} + * + * @param parentId The media ID of the parent item. + * @param children List (possibly empty) of child items. + */ + protected void onChildrenLoaded(@NonNull String parentId, + @NonNull List children) { + } + + /** + * Called when the {@link MediaBrowserServiceCompat} connection is lost. + */ + protected void onDisconnected() { + } + + @NonNull + protected final MediaControllerCompat getMediaController() { + if (mMediaController == null) { + throw new IllegalStateException("MediaController is null!"); + } + return mMediaController; + } + + /** + * The internal state of the app needs to revert to what it looks like when it started before + * any connections to the {@link AudiobookService} happens via the {@link MediaSessionCompat}. + */ + private void resetState() { + performOnAllCallbacks(callback -> callback.onPlaybackStateChanged(null)); + Log.d(TAG, "resetState: "); + } + + public MediaControllerCompat.TransportControls getTransportControls() { + if (mMediaController == null) { + Log.d(TAG, "getTransportControls: MediaController is null!"); + throw new IllegalStateException("MediaController is null!"); + } + return mMediaController.getTransportControls(); + } + + public void registerCallback(Callback callback) { + if (callback != null) { + mCallbackList.add(callback); + + // Update with the latest metadata/playback state. + if (mMediaController != null) { + final MediaMetadataCompat metadata = mMediaController.getMetadata(); + if (metadata != null) { + callback.onMetadataChanged(metadata); + } + + final PlaybackStateCompat playbackState = mMediaController.getPlaybackState(); + if (playbackState != null) { + callback.onPlaybackStateChanged(playbackState); + } + } + } + } + + private void performOnAllCallbacks(@NonNull CallbackCommand command) { + for (Callback callback : mCallbackList) { + if (callback != null) { + command.perform(callback); + } + } + } + + /** + * Helper for more easily performing operations on all listening clients. + */ + private interface CallbackCommand { + void perform(@NonNull Callback callback); + } + + // Receives callbacks from the MediaBrowser when it has successfully connected to the + // MediaBrowserService (AudiobookService). + private class MediaBrowserConnectionCallback extends MediaBrowserCompat.ConnectionCallback { + + // Happens as a result of onStart(). + @Override + public void onConnected() { + try { + // Get a MediaController for the MediaSession. + mMediaController = + new MediaControllerCompat(mContext, mMediaBrowser.getSessionToken()); + mMediaController.registerCallback(mMediaControllerCallback); + + // Sync existing MediaSession state to the UI. + mMediaControllerCallback.onMetadataChanged(mMediaController.getMetadata()); + mMediaControllerCallback.onPlaybackStateChanged( + mMediaController.getPlaybackState()); + + MediaBrowserHelper.this.onConnected(mMediaController); + } catch (RemoteException e) { + Log.d(TAG, String.format("onConnected: Problem: %s", e.toString())); + throw new RuntimeException(e); + } + + mMediaBrowser.subscribe(mMediaBrowser.getRoot(), mMediaBrowserSubscriptionCallback); + } + } + + // Receives callbacks from the MediaBrowser when the MediaBrowserService has loaded new media + // that is ready for playback. + public class MediaBrowserSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback { + + @Override + public void onChildrenLoaded(@NonNull String parentId, + @NonNull List children) { + MediaBrowserHelper.this.onChildrenLoaded(parentId, children); + } + } + + // Receives callbacks from the MediaController and updates the UI state, + // i.e.: Which is the current item, whether it's playing or paused, etc. + private class MediaControllerCallback extends MediaControllerCompat.Callback { + + @Override + public void onMetadataChanged(final MediaMetadataCompat metadata) { + performOnAllCallbacks(callback -> callback.onMetadataChanged(metadata)); + } + + @Override + public void onPlaybackStateChanged(@Nullable final PlaybackStateCompat state) { + performOnAllCallbacks(callback -> callback.onPlaybackStateChanged(state)); + } + + @Override + public void onExtrasChanged(Bundle extras) { + performOnAllCallbacks(callback -> callback.onExtrasChanged(extras)); + } + + // This might happen if the AudiobookService is killed while the Activity is in the + // foreground and onStart() has been called (but not onStop()). + @Override + public void onSessionDestroyed() { + resetState(); + onPlaybackStateChanged(null); + + MediaBrowserHelper.this.onDisconnected(); + } + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaNotificationManager.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaNotificationManager.java new file mode 100755 index 0000000..b11607b --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaNotificationManager.java @@ -0,0 +1,213 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * 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. + */ + +package com.moiseum.wolnelektury.view.player.service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.RequiresApi; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.app.NotificationCompat.MediaStyle; +import android.support.v4.media.session.MediaButtonReceiver; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import com.bumptech.glide.Glide; +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.view.main.MainActivity; + +import java.util.concurrent.ExecutionException; + + +/** + * Keeps track of a notification and updates it automatically for a given MediaSession. This is + * required so that the music service don't get killed during playback. + */ +public class MediaNotificationManager { + + public static final int NOTIFICATION_ID = 412; + + private static final String TAG = MediaNotificationManager.class.getSimpleName(); + private static final String CHANNEL_ID = "com.moiseum.wolnelektury.audiobookplayer.channel"; + private static final int REQUEST_CODE = 501; + + private final AudiobookService mService; + + private final NotificationCompat.Action mPlayAction; + private final NotificationCompat.Action mPauseAction; + private final NotificationCompat.Action mNextAction; + private final NotificationCompat.Action mPrevAction; + private final NotificationManager mNotificationManager; + + public MediaNotificationManager(AudiobookService service) { + mService = service; + + mNotificationManager = + (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE); + + mPlayAction = + new NotificationCompat.Action( + R.drawable.ic_play_arrow_white_24dp, + mService.getString(R.string.label_play), + MediaButtonReceiver.buildMediaButtonPendingIntent( + mService, + PlaybackStateCompat.ACTION_PLAY)); + mPauseAction = + new NotificationCompat.Action( + R.drawable.ic_pause_white_24dp, + mService.getString(R.string.label_pause), + MediaButtonReceiver.buildMediaButtonPendingIntent( + mService, + PlaybackStateCompat.ACTION_PAUSE)); + mNextAction = + new NotificationCompat.Action( + R.drawable.ic_skip_next_white_24dp, + mService.getString(R.string.label_next), + MediaButtonReceiver.buildMediaButtonPendingIntent( + mService, + PlaybackStateCompat.ACTION_SKIP_TO_NEXT)); + mPrevAction = + new NotificationCompat.Action( + R.drawable.ic_skip_previous_white_24dp, + mService.getString(R.string.label_previous), + MediaButtonReceiver.buildMediaButtonPendingIntent( + mService, + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)); + + // Cancel all notifications to handle the case where the Service was killed and + // restarted by the system. + mNotificationManager.cancelAll(); + } + + public void onDestroy() { + Log.d(TAG, "onDestroy: "); + } + + public NotificationManager getNotificationManager() { + return mNotificationManager; + } + + public Notification getNotification(MediaMetadataCompat metadata, + @NonNull PlaybackStateCompat state, + MediaSessionCompat.Token token) { + boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_PLAYING; + MediaDescriptionCompat description = metadata.getDescription(); + NotificationCompat.Builder builder = + buildNotification(state, token, isPlaying, description); + return builder.build(); + } + + private NotificationCompat.Builder buildNotification(@NonNull PlaybackStateCompat state, + MediaSessionCompat.Token token, + boolean isPlaying, + MediaDescriptionCompat description) { + + // Create the (mandatory) notification channel when running on Android Oreo. + if (isAndroidOOrHigher()) { + createChannel(); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(mService, CHANNEL_ID); + builder.setStyle( + new MediaStyle() + .setMediaSession(token) + .setShowActionsInCompactView(0, 1, 2) + // For backwards compatibility with Android L and earlier. + .setShowCancelButton(true) + .setCancelButtonIntent( + MediaButtonReceiver.buildMediaButtonPendingIntent( + mService, + PlaybackStateCompat.ACTION_STOP))) + .setColor(ContextCompat.getColor(mService, R.color.colorAccent)) + .setSmallIcon(R.drawable.ic_notification_player) + // Pending intent that is fired when user clicks on notification. + .setContentIntent(createContentIntent()) + // Title - Usually Song name. + .setContentTitle(description.getTitle()) + // Subtitle - Usually Artist name. + .setContentText(description.getSubtitle()) +// .setLargeIcon(AudiobookLibrary.getAlbumBitmap(mService, description.getMediaId())) + // When notification is deleted (when playback is paused and notification can be + // deleted) fire MediaButtonPendingIntent with ACTION_STOP. + .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent( + mService, PlaybackStateCompat.ACTION_STOP)) + // Show controls on lock screen even when user hides sensitive content. + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + + // If skip to next action is enabled. + if ((state.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { + builder.addAction(mPrevAction); + } + + builder.addAction(isPlaying ? mPauseAction : mPlayAction); + + // If skip to prev action is enabled. + if ((state.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { + builder.addAction(mNextAction); + } + + return builder; + } + + // Does nothing on versions of Android earlier than O. + @RequiresApi(Build.VERSION_CODES.O) + private void createChannel() { + if (mNotificationManager.getNotificationChannel(CHANNEL_ID) == null) { + // The user-visible name of the channel. + CharSequence name = "MediaSession"; + // The user-visible description of the channel. + String description = "MediaSession and MediaPlayer"; + int importance = NotificationManager.IMPORTANCE_LOW; + NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance); + // Configure the notification channel. + mChannel.setDescription(description); + mChannel.enableLights(true); + // Sets the notification light color for notifications posted to this + // channel, if the device supports this feature. + mChannel.setLightColor(Color.RED); + mChannel.enableVibration(true); + mChannel.setVibrationPattern( + new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400}); + mNotificationManager.createNotificationChannel(mChannel); + Log.d(TAG, "createChannel: New channel created"); + } else { + Log.d(TAG, "createChannel: Existing channel reused"); + } + } + + private boolean isAndroidOOrHigher() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + } + + private PendingIntent createContentIntent() { + Intent openUI = new Intent(mService, MainActivity.class); + openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + return PendingIntent.getActivity( + mService, REQUEST_CODE, openUI, PendingIntent.FLAG_CANCEL_CURRENT); + } + +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaPlayerAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaPlayerAdapter.java new file mode 100755 index 0000000..2bb3a57 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaPlayerAdapter.java @@ -0,0 +1,327 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * 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. + */ + +package com.moiseum.wolnelektury.view.player.service; + +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.media.MediaPlayer; +import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Log; + +import com.moiseum.wolnelektury.connection.downloads.FileCacheUtils; + +/** + * Exposes the functionality of the {@link MediaPlayer} and implements the {@link PlayerAdapter} + * so that MainActivity can control music playback. + */ +public final class MediaPlayerAdapter extends PlayerAdapter { + + private class MediaPlayerException extends Exception { + + private final int what; + private final int extra; + + public MediaPlayerException(int what, int extra) { + this.what = what; + this.extra = extra; + } + + @Override + public String getMessage() { + return "Media player failed with code (" + what + "," + extra + ")"; + } + } + + private static final String TAG = MediaPlayerAdapter.class.getSimpleName(); + private static final int FIVE_SECONDS = 5000; + private static final int CALLBACK_INTERVAL = 400; + + private final Context mContext; + private MediaPlayer mMediaPlayer; + private String mFilename; + private PlaybackInfoListener mPlaybackInfoListener; + private MediaMetadataCompat mCurrentMedia; + private int mState; + private boolean mCurrentMediaPlayedToCompletion; + + private Handler handler = new Handler(); + private Runnable positionUpdateRunnable = new Runnable() { + @Override + public void run() { + mPlaybackInfoListener.onPlaybackProgress(mMediaPlayer.getCurrentPosition()); + handler.postDelayed(this, CALLBACK_INTERVAL); + } + }; + + // Work-around for a MediaPlayer bug related to the behavior of MediaPlayer.seekTo() + // while not playing. + private int mSeekWhileNotPlaying = -1; + + MediaPlayerAdapter(Context context, PlaybackInfoListener listener) { + super(context); + mContext = context.getApplicationContext(); + mPlaybackInfoListener = listener; + } + + /** + * Once the {@link MediaPlayer} is released, it can't be used again, and another one has to be + * created. In the onStop() method of the MainActivity the {@link MediaPlayer} is + * released. Then in the onStart() of the MainActivity a new {@link MediaPlayer} + * object has to be created. That's why this method is private, and called by load(int) and + * not the constructor. + */ + private void initializeMediaPlayer() { + if (mMediaPlayer == null) { + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setOnCompletionListener(mediaPlayer -> { + mPlaybackInfoListener.onPlaybackCompleted(); + + // Set the state to "paused" because it most closely matches the state + // in MediaPlayer with regards to available state transitions compared + // to "stop". + // Paused allows: seekTo(), start(), pause(), stop() + // Stop allows: stop() + setNewState(PlaybackStateCompat.STATE_PAUSED); + }); + mMediaPlayer.setOnErrorListener((mp, what, extra) -> { + Log.e(TAG, "Media player experienced failure: " + what + ", " + extra); + setNewState(PlaybackStateCompat.STATE_ERROR); + return false; + }); + } + } + + // Implements PlaybackControl. + @Override + public void playFromMedia(MediaMetadataCompat metadata) { + mCurrentMedia = metadata; + final String mediaId = metadata.getDescription().getMediaId(); + playFile(AudiobookLibrary.getMusicFilename(mediaId)); + } + + @Override + public MediaMetadataCompat getCurrentMedia() { + return mCurrentMedia; + } + + private void playFile(String filename) { + boolean mediaChanged = (mFilename == null || !filename.equals(mFilename)); + if (mCurrentMediaPlayedToCompletion) { + // Last audio file was played to completion, the resourceId hasn't changed, but the + // player was released, so force a reload of the media file for playback. + mediaChanged = true; + mCurrentMediaPlayedToCompletion = false; + } + if (!mediaChanged) { + if (!isPlaying()) { + play(); + } + return; + } else { + release(); + } + + mFilename = filename; + + initializeMediaPlayer(); + + try { + String cachedFileForUrl = FileCacheUtils.getCachedFileForUrl(mFilename); + mMediaPlayer.setDataSource(cachedFileForUrl); + mMediaPlayer.prepare(); + mPlaybackInfoListener.onPlaybackPrepared(mMediaPlayer.getDuration()); + } catch (Exception e) { + Log.e(TAG, "Failed to load file: " + mFilename, e); + setNewState(PlaybackStateCompat.STATE_ERROR); + } + + play(); + } + + @Override + public void onStop() { + // Regardless of whether or not the MediaPlayer has been created / started, the state must + // be updated, so that MediaNotificationManager can take down the notification. + setNewState(PlaybackStateCompat.STATE_STOPPED); + release(); + } + + private void release() { + if (mMediaPlayer != null) { + handler.removeCallbacks(positionUpdateRunnable); + mMediaPlayer.release(); + mMediaPlayer = null; + } + } + + @Override + public boolean isPlaying() { + return mMediaPlayer != null && mMediaPlayer.isPlaying(); + } + + @Override + protected void onPlay() { + if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { + mMediaPlayer.start(); + handler.postDelayed(positionUpdateRunnable, CALLBACK_INTERVAL); + setNewState(PlaybackStateCompat.STATE_PLAYING); + } + } + + @Override + protected void onPause() { + if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + handler.removeCallbacks(positionUpdateRunnable); + setNewState(PlaybackStateCompat.STATE_PAUSED); + } + } + + // This is the main reducer for the player state machine. + private void setNewState(@PlaybackStateCompat.State int newPlayerState) { + mState = newPlayerState; + if (mState == PlaybackStateCompat.STATE_ERROR) { + handler.removeCallbacks(positionUpdateRunnable); + mFilename = null; + } + + // Whether playback goes to completion, or whether it is stopped, the + // mCurrentMediaPlayedToCompletion is set to true. + if (mState == PlaybackStateCompat.STATE_STOPPED) { + mCurrentMediaPlayedToCompletion = true; + } + + // Work around for MediaPlayer.getCurrentPosition() when it changes while not playing. + final long reportPosition; + if (mSeekWhileNotPlaying >= 0) { + reportPosition = mSeekWhileNotPlaying; + + if (mState == PlaybackStateCompat.STATE_PLAYING) { + mSeekWhileNotPlaying = -1; + } + } else { + reportPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition(); + } + + final PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder(); + stateBuilder.setActions(getAvailableActions()); + stateBuilder.setState(mState, + reportPosition, + 1.0f, + SystemClock.elapsedRealtime()); + if (mState == PlaybackStateCompat.STATE_PLAYING) { + Bundle extras = new Bundle(); + extras.putInt(AudiobookService.EXTRA_PLAYBACK_TOTAL, mMediaPlayer.getDuration()); + stateBuilder.setExtras(extras); + } + mPlaybackInfoListener.onPlaybackStateChange(stateBuilder.build()); + } + + /** + * Set the current capabilities available on this session. Note: If a capability is not + * listed in the bitmask of capabilities then the MediaSession will not handle it. For + * example, if you don't want ACTION_STOP to be handled by the MediaSession, then don't + * included it in the bitmask that's returned. + */ + @PlaybackStateCompat.Actions + private long getAvailableActions() { + long actions = PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + switch (mState) { + case PlaybackStateCompat.STATE_STOPPED: + actions |= PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE; + break; + case PlaybackStateCompat.STATE_PLAYING: + actions |= PlaybackStateCompat.ACTION_STOP + | PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_SEEK_TO; + break; + case PlaybackStateCompat.STATE_PAUSED: + actions |= PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_STOP; + break; + default: + actions |= PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_STOP + | PlaybackStateCompat.ACTION_PAUSE; + } + return actions; + } + + @Override + public void seekTo(long position) { + if (mMediaPlayer != null) { + if (!mMediaPlayer.isPlaying()) { + mSeekWhileNotPlaying = (int) position; + } + mMediaPlayer.seekTo((int) position); + + // Set the state (to the current state) because the position changed and should + // be reported to clients. + setNewState(mState); + } + } + + @Override + public void fastForward() { + if (mMediaPlayer != null) { + int seekTo = mMediaPlayer.getCurrentPosition() + FIVE_SECONDS; + int newState = mState; + + if (seekTo > mMediaPlayer.getDuration()) { + seekTo = mMediaPlayer.getDuration(); + newState = PlaybackStateCompat.STATE_PAUSED; + mMediaPlayer.pause(); + } + + mMediaPlayer.seekTo(seekTo); + setNewState(newState); + } + } + + @Override + public void rewind() { + if (mMediaPlayer != null) { + int seekTo = mMediaPlayer.getCurrentPosition() - FIVE_SECONDS; + int newState = mState; + + if (seekTo < 0) { + seekTo = 0; + newState = PlaybackStateCompat.STATE_PAUSED; + mMediaPlayer.pause(); + } + + mMediaPlayer.seekTo(seekTo); + setNewState(newState); + } + } + + @Override + public void setVolume(float volume) { + if (mMediaPlayer != null) { + mMediaPlayer.setVolume(volume, volume); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/PlaybackInfoListener.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/PlaybackInfoListener.java new file mode 100755 index 0000000..18dc813 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/PlaybackInfoListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * 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. + */ + +package com.moiseum.wolnelektury.view.player.service; + +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; + +/** + * Listener to provide state updates from {@link MediaPlayerAdapter} (the media player) + * to {@link AudiobookService} (the service that holds our {@link MediaSessionCompat}. + */ +public abstract class PlaybackInfoListener { + + public abstract void onPlaybackStateChange(PlaybackStateCompat state); + + public void onPlaybackCompleted() { + } + + public void onPlaybackProgress(int current) { + } + + public void onPlaybackPrepared(int duration) { + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/PlayerAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/PlayerAdapter.java new file mode 100755 index 0000000..1182a11 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/PlayerAdapter.java @@ -0,0 +1,175 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * 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. + */ + +package com.moiseum.wolnelektury.view.player.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.support.annotation.NonNull; +import android.support.v4.media.MediaMetadataCompat; + +/** + * Abstract player implementation that handles playing music with proper handling of headphones + * and audio focus. + */ +public abstract class PlayerAdapter { + + private static final float MEDIA_VOLUME_DEFAULT = 1.0f; + private static final float MEDIA_VOLUME_DUCK = 0.2f; + + private static final IntentFilter AUDIO_NOISY_INTENT_FILTER = + new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + + private boolean mAudioNoisyReceiverRegistered = false; + private final BroadcastReceiver mAudioNoisyReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { + if (isPlaying()) { + pause(); + } + } + } + }; + + private final Context mApplicationContext; + private final AudioManager mAudioManager; + private final AudioFocusHelper mAudioFocusHelper; + + private boolean mPlayOnAudioFocus = false; + + public PlayerAdapter(@NonNull Context context) { + mApplicationContext = context.getApplicationContext(); + mAudioManager = (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE); + mAudioFocusHelper = new AudioFocusHelper(); + } + + public abstract void playFromMedia(MediaMetadataCompat metadata); + + public abstract MediaMetadataCompat getCurrentMedia(); + + public abstract boolean isPlaying(); + + public final void play() { + if (mAudioFocusHelper.requestAudioFocus()) { + registerAudioNoisyReceiver(); + onPlay(); + } + } + + /** + * Called when media is ready to be played and indicates the app has audio focus. + */ + protected abstract void onPlay(); + + public final void pause() { + if (!mPlayOnAudioFocus) { + mAudioFocusHelper.abandonAudioFocus(); + } + + unregisterAudioNoisyReceiver(); + onPause(); + } + + /** + * Called when media must be paused. + */ + protected abstract void onPause(); + + public final void stop() { + mAudioFocusHelper.abandonAudioFocus(); + unregisterAudioNoisyReceiver(); + onStop(); + } + + /** + * Called when the media must be stopped. The player should clean up resources at this + * point. + */ + protected abstract void onStop(); + + public abstract void seekTo(long position); + + public abstract void fastForward(); + + public abstract void rewind(); + + public abstract void setVolume(float volume); + + private void registerAudioNoisyReceiver() { + if (!mAudioNoisyReceiverRegistered) { + mApplicationContext.registerReceiver(mAudioNoisyReceiver, AUDIO_NOISY_INTENT_FILTER); + mAudioNoisyReceiverRegistered = true; + } + } + + private void unregisterAudioNoisyReceiver() { + if (mAudioNoisyReceiverRegistered) { + mApplicationContext.unregisterReceiver(mAudioNoisyReceiver); + mAudioNoisyReceiverRegistered = false; + } + } + + /** + * Helper class for managing audio focus related tasks. + */ + private final class AudioFocusHelper + implements AudioManager.OnAudioFocusChangeListener { + + private boolean requestAudioFocus() { + final int result = mAudioManager.requestAudioFocus(this, + AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; + } + + private void abandonAudioFocus() { + mAudioManager.abandonAudioFocus(this); + } + + @Override + public void onAudioFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + if (mPlayOnAudioFocus && !isPlaying()) { + play(); + } else if (isPlaying()) { + setVolume(MEDIA_VOLUME_DEFAULT); + } + mPlayOnAudioFocus = false; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + setVolume(MEDIA_VOLUME_DUCK); + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + if (isPlaying()) { + mPlayOnAudioFocus = true; + pause(); + } + break; + case AudioManager.AUDIOFOCUS_LOSS: + mAudioManager.abandonAudioFocus(this); + mPlayOnAudioFocus = false; + stop(); + break; + } + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchFiltersAdapter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchFiltersAdapter.java new file mode 100644 index 0000000..e74879f --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchFiltersAdapter.java @@ -0,0 +1,49 @@ +package com.moiseum.wolnelektury.view.search; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter; +import com.moiseum.wolnelektury.components.recycler.ViewHolder; +import com.moiseum.wolnelektury.connection.models.CategoryModel; + +import butterknife.BindView; + +/** + * Created by Piotr Ostrowski on 27.11.2017. + */ + +public class BookSearchFiltersAdapter extends RecyclerAdapter { + + static class FilterViewHolder extends ViewHolder { + + @BindView(R.id.tvFilterName) + TextView tvFilterName; + + FilterViewHolder(View view) { + super(view); + } + + @Override + public void bind(CategoryModel item, boolean selected) { + tvFilterName.setText(item.getName()); + } + } + + public BookSearchFiltersAdapter(Context context) { + super(context, Selection.NONE); + } + + @Override + protected String getItemId(CategoryModel item) { + return item.getSlug(); + } + + @Override + public FilterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new FilterViewHolder(inflate(R.layout.filter_item, parent)); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchFragment.java new file mode 100644 index 0000000..c29522f --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchFragment.java @@ -0,0 +1,254 @@ +package com.moiseum.wolnelektury.view.search; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.Toast; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.components.ProgressRecyclerView; +import com.moiseum.wolnelektury.components.recycler.EndlessRecyclerOnScrollListener; +import com.moiseum.wolnelektury.components.recycler.RecyclerAdapter; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.CategoryModel; +import com.moiseum.wolnelektury.view.book.BookActivity; +import com.moiseum.wolnelektury.view.book.BookType; +import com.moiseum.wolnelektury.view.book.list.BooksListAdapter; +import com.moiseum.wolnelektury.view.search.dto.FilterDto; +import com.moiseum.wolnelektury.view.search.filter.FilterActivity; + +import org.parceler.Parcels; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.OnClick; + +import static android.app.Activity.RESULT_OK; +import static com.moiseum.wolnelektury.view.search.filter.FilterActivity.FILTERS_REQUEST_CODE; +import static com.moiseum.wolnelektury.view.search.filter.FilterActivity.RESULT_FILTERS_KEY; + +/** + * Created by piotrostrowski on 16.11.2017. + */ + +public class BookSearchFragment extends PresenterFragment implements BookSearchView, SearchView.OnQueryTextListener { + + public static BookSearchFragment newInstance() { + return new BookSearchFragment(); + } + + @BindView(R.id.rlFiltersContainer) + RelativeLayout rlFiltersContainer; + @BindView(R.id.rvFilters) + RecyclerView rvFilters; + @BindView(R.id.rvBooks) + ProgressRecyclerView rvBooks; + @BindView(R.id.pbLoadMore) + ProgressBar pbLoadMore; + @BindView(R.id.btnReloadMore) + Button btnReloadMore; + + private SearchView svSearch; + + private BookSearchFiltersAdapter filtersAdapter; + private RecyclerAdapter.OnItemClickListener filtersAdapterClickListener = new RecyclerAdapter + .OnItemClickListener() { + @Override + public void onItemClicked(CategoryModel item, View view, int position) { + filtersAdapter.removeItem(position); + if (filtersAdapter.getItemCount() == 0) { + rlFiltersContainer.setVisibility(View.GONE); + } + getPresenter().removeFilter(item); + } + }; + + private BooksListAdapter searchAdapter; + private EndlessRecyclerOnScrollListener rvBooksScrollListener = new EndlessRecyclerOnScrollListener() { + @Override + public void onLoadMore() { + if (searchAdapter.getItemCount() > 0) { + BookModel lastItem = searchAdapter.getItem(searchAdapter.getItemCount() - 1); + getPresenter().loadMoreBooks(lastItem.getKey()); + } + } + }; + private RecyclerAdapter.OnItemClickListener searchAdapterClickListener = (item, view, position) -> getPresenter().bookSelected(item, position); + + @Override + protected BookSearchPresenter createPresenter() { + return new BookSearchPresenter(this); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_search; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + // Filters + rvFilters.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); + rvFilters.setHasFixedSize(false); + filtersAdapter = new BookSearchFiltersAdapter(getContext()); + filtersAdapter.setOnItemClickListener(filtersAdapterClickListener); + rvFilters.setAdapter(filtersAdapter); + + // Search list + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + searchAdapter = new BooksListAdapter(getContext()); + searchAdapter.setOnItemClickListener(searchAdapterClickListener); + rvBooks.setLayoutManager(layoutManager); + rvBooks.addOnScrollListener(rvBooksScrollListener); + rvBooks.setHasFixedSize(true); + rvBooks.setAdapter(searchAdapter); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.menu_search, menu); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + MenuItem item = menu.findItem(R.id.action_search); + svSearch = (SearchView) item.getActionView(); + svSearch.setIconifiedByDefault(false); + svSearch.setIconified(false); + svSearch.setQueryRefinementEnabled(true); + svSearch.setOnQueryTextListener(this); + item.expandActionView(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_filter) { + getPresenter().onFiltersClicked(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onQueryTextSubmit(String query) { + getPresenter().onSearchChosen(query); + svSearch.clearFocus(); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + getPresenter().onSearchChanged(newText); + return true; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == FILTERS_REQUEST_CODE && resultCode == RESULT_OK && data != null && data.hasExtra(RESULT_FILTERS_KEY)) { + FilterDto dto = Parcels.unwrap(data.getParcelableExtra(RESULT_FILTERS_KEY)); + getPresenter().updateFilters(dto); + } + super.onActivityResult(requestCode, resultCode, data); + + } + + @Override + public void onDestroyView() { + rvBooks.removeOnScrollListener(rvBooksScrollListener); + super.onDestroyView(); + } + + @OnClick(R.id.ibClearFilters) + public void onClearFiltersClick() { + filtersAdapter.setItems(new ArrayList<>()); + rlFiltersContainer.setVisibility(View.GONE); + getPresenter().clearFilters(); + } + + @OnClick(R.id.btnReloadMore) + public void onReloadMoreClicked() { + btnReloadMore.setVisibility(View.GONE); + pbLoadMore.setVisibility(View.VISIBLE); + + BookModel lastItem = searchAdapter.getItem(searchAdapter.getItemCount() - 1); + getPresenter().loadMoreBooks(lastItem.getKey()); + } + + @Override + public void setData(List books, boolean reload) { + if (reload) { + rvBooks.setItems(books); + } else { + rvBooks.addItems(books); + } + } + + @Override + public void setInitialProgressVisible(boolean visible) { + rvBooksScrollListener.reset(); + searchAdapter.clear(); + rvBooks.setProgressVisible(visible); + } + + @Override + public void setLoadMoreProgressVisible(boolean visible) { + pbLoadMore.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + @Override + public void showError(Exception e, boolean loadMore) { + Toast.makeText(getContext(), R.string.loading_results_failed, Toast.LENGTH_SHORT).show(); + if (loadMore) { + btnReloadMore.setVisibility(View.VISIBLE); + } else { + searchAdapter.clear(); + rvBooks.showRetryButton(() -> getPresenter().retryInitialLoad()); + } + } + + @Override + public void presentBookDetails(String bookSlug) { + startActivity(new BookActivity.BookIntent(bookSlug, BookType.TYPE_DEFAULT, getContext())); + } + + @Override + public void applyFilters(List filters) { + filtersAdapter.setItems(filters); + rlFiltersContainer.setVisibility((filters.size() > 0) ? View.VISIBLE : View.GONE); + } + + @Override + public void displayFiltersView(FilterDto filters) { + FilterActivity.FilterIntent filterIntent = new FilterActivity.FilterIntent(getContext(), filters); + startActivityForResult(filterIntent, FILTERS_REQUEST_CODE); + } + + @Override + public void updateFavouriteState(boolean state, Integer clickedPosition) { + if (clickedPosition != null) { + searchAdapter.getItem(clickedPosition).setLiked(state); + searchAdapter.notifyDataSetChanged(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchPresenter.java new file mode 100644 index 0000000..fef67ac --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchPresenter.java @@ -0,0 +1,223 @@ +package com.moiseum.wolnelektury.view.search; + +import android.os.Bundle; +import android.os.Handler; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.RestClientCallback; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.CategoryModel; +import com.moiseum.wolnelektury.connection.services.BooksService; +import com.moiseum.wolnelektury.utils.StringUtils; +import com.moiseum.wolnelektury.events.BookFavouriteEvent; +import com.moiseum.wolnelektury.view.search.dto.FilterDto; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; + +/** + * Created by piotrostrowski on 16.11.2017. + */ + +class BookSearchPresenter extends FragmentPresenter { + + private static final String ONLY_LECTURES_SLUG = "only_lectures_slug"; + private static final String HAS_AUDIOBOOK_SLUG = "has_audiobook_slug"; + private static final long TWO_SECONDS = 2000; + + private final String lectureFilterName; + private final String audiobookFilterName; + + private RestClient client = WLApplication.getInstance().getRestClient(); + + private FilterDto filters; + private String lastKey; + private String searchQuery; + private String temporarySearchText; + private Integer clickedPosition; + + private Call> currentCall; + + private Handler searchHandler; + private Runnable searchHandlerRunnable = new Runnable() { + @Override + public void run() { + searchQuery = temporarySearchText; + loadBooks(true); + } + }; + + BookSearchPresenter(BookSearchView view) { + super(view); + lectureFilterName = getView().getContext().getString(R.string.lectures); + audiobookFilterName = getView().getContext().getString(R.string.audiobook); + + filters = new FilterDto(); + lastKey = null; + searchQuery = null; + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + loadBooks(true); + EventBus.getDefault().register(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (currentCall != null) { + currentCall.cancel(); + } + if (searchHandler != null) { + searchHandler.removeCallbacks(searchHandlerRunnable); + } + EventBus.getDefault().unregister(this); + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFavouriteStateChanged(BookFavouriteEvent event) { + getView().updateFavouriteState(event.getState(), clickedPosition); + } + + void bookSelected(BookModel item, int position) { + clickedPosition = position; + getView().presentBookDetails(item.getSlug()); + } + + void updateFilters(FilterDto dto) { + filters = dto; + List flatteredFilters = new ArrayList<>(); + if (dto.isLecture()) { + CategoryModel lectureModel = new CategoryModel(); + lectureModel.setName(lectureFilterName); + lectureModel.setSlug(ONLY_LECTURES_SLUG); + flatteredFilters.add(lectureModel); + } + if (dto.isAudiobook()) { + CategoryModel audiobookModel = new CategoryModel(); + audiobookModel.setName(audiobookFilterName); + audiobookModel.setSlug(HAS_AUDIOBOOK_SLUG); + flatteredFilters.add(audiobookModel); + } + flatteredFilters.addAll(dto.getFilteredEpochs()); + flatteredFilters.addAll(dto.getFilteredGenres()); + flatteredFilters.addAll(dto.getFilteredKinds()); + getView().applyFilters(flatteredFilters); + loadBooks(true); + } + + void removeFilter(CategoryModel item) { + if (ONLY_LECTURES_SLUG.equals(item.getSlug())) { + filters.setLecture(false); + } else if (HAS_AUDIOBOOK_SLUG.equals(item.getSlug())) { + filters.setAudiobook(false); + } else if (filters.getFilteredEpochs().contains(item)) { + filters.getFilteredEpochs().remove(item); + } else if (filters.getFilteredGenres().contains(item)) { + filters.getFilteredGenres().remove(item); + } else if (filters.getFilteredKinds().contains(item)) { + filters.getFilteredKinds().remove(item); + } + loadBooks(true); + } + + void onFiltersClicked() { + getView().displayFiltersView(filters); + } + + void clearFilters() { + filters = new FilterDto(); + lastKey = null; + loadBooks(true); + } + + void loadMoreBooks(final String key) { + lastKey = key; + loadBooks(false); + } + + void onSearchChosen(String query) { + if (query.length() == 0) { + query = null; + } + searchQuery = query; + loadBooks(true); + } + + void onSearchChanged(String newText) { + if (searchHandler != null) { + searchHandler.removeCallbacks(searchHandlerRunnable); + } + + if (newText.length() == 0) { + onSearchChosen(newText); + } else { + temporarySearchText = newText; + searchHandler = new Handler(); + searchHandler.postDelayed(searchHandlerRunnable, TWO_SECONDS); + } + } + + void retryInitialLoad() { + loadBooks(true); + } + + private void loadBooks(final boolean reset) { + if (reset) { + lastKey = null; + getView().setInitialProgressVisible(true); + } else { + getView().setLoadMoreProgressVisible(true); + } + currentCall = client.call(new RestClientCallback, BooksService>() { + @Override + public void onSuccess(List data) { + hideProgress(); + getView().setData(data, reset); + } + + @Override + public void onFailure(Exception e) { + hideProgress(); + getView().showError(e, false); + } + + @Override + public void onCancel() { + //nop + } + + @Override + public Call> execute(BooksService service) { + return service.getSearchBooks(searchQuery, + StringUtils.joinSlugs(filters.getFilteredEpochs(), ","), + StringUtils.joinSlugs(filters.getFilteredGenres(), ","), + StringUtils.joinSlugs(filters.getFilteredKinds(), ","), + filters.isAudiobook() ? true : null, + filters.isLecture() ? true : null, + lastKey, + RestClient.PAGINATION_LIMIT); + } + + private void hideProgress() { + if (reset) { + getView().setInitialProgressVisible(false); + } else { + getView().setLoadMoreProgressVisible(false); + } + } + }, BooksService.class); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchView.java new file mode 100644 index 0000000..d2c873c --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchView.java @@ -0,0 +1,27 @@ +package com.moiseum.wolnelektury.view.search; + +import android.content.Context; + +import com.moiseum.wolnelektury.base.mvp.PaginableLoadingView; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.connection.models.CategoryModel; +import com.moiseum.wolnelektury.view.search.dto.FilterDto; + +import java.util.List; + +/** + * Created by piotrostrowski on 16.11.2017. + */ + +public interface BookSearchView extends PaginableLoadingView> { + + void presentBookDetails(String bookSlug); + + void applyFilters(List filters); + + void displayFiltersView(FilterDto filters); + + void updateFavouriteState(boolean state, Integer clickedPosition); + + Context getContext(); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/components/EmptySupportRecyclerView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/components/EmptySupportRecyclerView.java new file mode 100644 index 0000000..dec9b6d --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/components/EmptySupportRecyclerView.java @@ -0,0 +1,60 @@ +package com.moiseum.wolnelektury.view.search.components; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.View; + +/** + * Created by piotrostrowski on 17.07.2017. + */ + +public class EmptySupportRecyclerView extends RecyclerView { + private View emptyView; + + private AdapterDataObserver emptyObserver = new AdapterDataObserver() { + + + @Override + public void onChanged() { + RecyclerView.Adapter adapter = getAdapter(); + if (adapter != null && emptyView != null) { + if (adapter.getItemCount() == 0) { + emptyView.setVisibility(View.VISIBLE); + EmptySupportRecyclerView.this.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + EmptySupportRecyclerView.this.setVisibility(View.VISIBLE); + } + } + + } + }; + + public EmptySupportRecyclerView(Context context) { + super(context); + } + + public EmptySupportRecyclerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EmptySupportRecyclerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setAdapter(Adapter adapter) { + super.setAdapter(adapter); + + if (adapter != null) { + adapter.registerAdapterDataObserver(emptyObserver); + } + + emptyObserver.onChanged(); + } + + public void setEmptyView(View emptyView) { + this.emptyView = emptyView; + } +} \ No newline at end of file diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/components/FiltersProgressFlowLayout.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/components/FiltersProgressFlowLayout.java new file mode 100644 index 0000000..529968c --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/components/FiltersProgressFlowLayout.java @@ -0,0 +1,132 @@ +package com.moiseum.wolnelektury.view.search.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.AppCompatCheckBox; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.connection.models.CategoryModel; +import com.nex3z.flowlayout.FlowLayout; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +/** + * Created by piotrostrowski on 26.11.2017. + */ + +public class FiltersProgressFlowLayout extends RelativeLayout { + + public interface FiltersProgressFlowLayoutRetryListener { + void onRetryClicked(); + } + + @BindView(R.id.flList) + FlowLayout flList; + @BindView(R.id.tvEmpty) + TextView tvEmpty; + @BindView(R.id.pbLoading) + ProgressBar pbLoading; + @BindView(R.id.ibRetry) + ImageButton ibRetry; + + private List categories = new ArrayList<>(); + private CompoundButton.OnCheckedChangeListener checkChangeListener = new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + for (CategoryModel category : categories) { + String slug = (String) buttonView.getTag(); + if (slug.equals(category.getSlug())) { + category.setChecked(isChecked); + return; + } + } + } + }; + private FiltersProgressFlowLayoutRetryListener listener; + + public FiltersProgressFlowLayout(@NonNull Context context) { + this(context, null); + } + + public FiltersProgressFlowLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public FiltersProgressFlowLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs); + } + + private void init(AttributeSet attrs) { + View view = LayoutInflater.from(getContext()).inflate(R.layout.progress_flowlayout, this, true); + ButterKnife.bind(this, view); + + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.FiltersProgressFlowLayout); + try { + if (a.hasValue(R.styleable.FiltersProgressFlowLayout_emptyLayoutText)) { + tvEmpty.setText(a.getString(R.styleable.FiltersProgressFlowLayout_emptyLayoutText)); + } + } finally { + a.recycle(); + } + } + + public void addCategories(List categories) { + this.categories = categories; + LayoutInflater inflater = LayoutInflater.from(getContext()); + for (CategoryModel categoryModel : categories) { + AppCompatCheckBox checkBox = (AppCompatCheckBox) inflater.inflate(R.layout.checkbox, flList, false); + checkBox.setText(categoryModel.getName()); + checkBox.setTag(categoryModel.getSlug()); + checkBox.setChecked(categoryModel.isChecked()); + checkBox.setOnCheckedChangeListener(checkChangeListener); + flList.addView(checkBox); + } + pbLoading.setVisibility(GONE); + flList.setVisibility(VISIBLE); + } + + public List getSelectedCategories() { + List selectedCategories = new ArrayList<>(); + for (CategoryModel categoryModel : categories) { + if (categoryModel.isChecked()) { + selectedCategories.add(categoryModel); + } + } + return selectedCategories; + } + + public void showRetryButton(FiltersProgressFlowLayoutRetryListener listener) { + this.listener = listener; + tvEmpty.setVisibility(GONE); + pbLoading.setVisibility(GONE); + ibRetry.setVisibility(VISIBLE); + } + + @OnClick(R.id.ibRetry) + public void retryButtonClick() { + if (listener != null) { + listener.onRetryClicked(); + } + ibRetry.setVisibility(GONE); + pbLoading.setVisibility(VISIBLE); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/dto/FilterDto.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/dto/FilterDto.java new file mode 100644 index 0000000..4e25726 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/dto/FilterDto.java @@ -0,0 +1,68 @@ +package com.moiseum.wolnelektury.view.search.dto; + +import com.moiseum.wolnelektury.connection.models.CategoryModel; + +import org.parceler.Parcel; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by piotrostrowski on 26.11.2017. + */ + +@Parcel(Parcel.Serialization.BEAN) +public class FilterDto { + + private boolean isLecture; + private boolean audiobook; + private List filteredEpochs; + private List filteredGenres; + private List filteredKinds; + + public FilterDto() { + this.filteredEpochs = new ArrayList<>(); + this.filteredGenres = new ArrayList<>(); + this.filteredKinds = new ArrayList<>(); + } + + public boolean isLecture() { + return isLecture; + } + + public void setLecture(boolean lecture) { + isLecture = lecture; + } + + public boolean isAudiobook() { + return audiobook; + } + + public void setAudiobook(boolean audiobook) { + this.audiobook = audiobook; + } + + public List getFilteredEpochs() { + return filteredEpochs; + } + + public void setFilteredEpochs(List filteredEpochs) { + this.filteredEpochs = filteredEpochs; + } + + public List getFilteredGenres() { + return filteredGenres; + } + + public void setFilteredGenres(List filteredGenres) { + this.filteredGenres = filteredGenres; + } + + public List getFilteredKinds() { + return filteredKinds; + } + + public void setFilteredKinds(List filteredKinds) { + this.filteredKinds = filteredKinds; + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterActivity.java new file mode 100644 index 0000000..0c394f9 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterActivity.java @@ -0,0 +1,51 @@ +package com.moiseum.wolnelektury.view.search.filter; + +import android.content.Context; +import android.os.Bundle; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.base.AbstractIntent; +import com.moiseum.wolnelektury.view.search.dto.FilterDto; + +import org.parceler.Parcels; + +/** + * Created by piotrostrowski on 25.11.2017. + */ + +public class FilterActivity extends AbstractActivity { + + private static final String FILTER_FRAGMENT_TAG = "FilterFragmentTag"; + public static final String RESULT_FILTERS_KEY = "ResultFiltersKey"; + public static final String FILTERS_KEY = "FiltersKey"; + public static final int FILTERS_REQUEST_CODE = 105; + + public static class FilterIntent extends AbstractIntent { + + public FilterIntent(Context packageContext, FilterDto dto) { + super(packageContext, FilterActivity.class); + putExtra(FILTERS_KEY, Parcels.wrap(dto)); + } + } + + @Override + public int getLayoutResourceId() { + return R.layout.activity_blank; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + setTitle(R.string.filters); + + FilterFragment filterFragment = (FilterFragment) getSupportFragmentManager().findFragmentByTag(FILTER_FRAGMENT_TAG); + if (filterFragment == null) { + FilterDto filters = Parcels.unwrap(getIntent().getParcelableExtra(FILTERS_KEY)); + filterFragment = FilterFragment.newInstance(filters); + getSupportFragmentManager().beginTransaction().add(R.id.flContainer, filterFragment, FILTER_FRAGMENT_TAG).commit(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterFragment.java new file mode 100644 index 0000000..4ae7718 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterFragment.java @@ -0,0 +1,154 @@ +package com.moiseum.wolnelektury.view.search.filter; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.widget.SwitchCompat; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.connection.models.CategoryModel; +import com.moiseum.wolnelektury.view.search.components.FiltersProgressFlowLayout; +import com.moiseum.wolnelektury.view.search.dto.FilterDto; + +import org.parceler.Parcels; + +import java.util.List; + +import butterknife.BindView; + +import static com.moiseum.wolnelektury.view.search.filter.FilterActivity.FILTERS_KEY; +import static com.moiseum.wolnelektury.view.search.filter.FilterActivity.RESULT_FILTERS_KEY; + +/** + * Created by piotrostrowski on 25.11.2017. + */ + +public class FilterFragment extends PresenterFragment implements FilterView { + + @BindView(R.id.swLecturesOnly) + SwitchCompat swLecturesOnly; + @BindView(R.id.swHasAudiobook) + SwitchCompat swHasAudiobook; + @BindView(R.id.pflEpochs) + FiltersProgressFlowLayout pflEpochs; + @BindView(R.id.pflGenres) + FiltersProgressFlowLayout pflGenres; + @BindView(R.id.pflKinds) + FiltersProgressFlowLayout pflKinds; + + public static FilterFragment newInstance(FilterDto filters) { + FilterFragment fragment = new FilterFragment(); + Bundle args = new Bundle(); + args.putParcelable(FILTERS_KEY, Parcels.wrap(filters)); + fragment.setArguments(args); + return fragment; + } + + @Override + protected FilterPresenter createPresenter() { + FilterDto filters = Parcels.unwrap(getArguments().getParcelable(FILTERS_KEY)); + return new FilterPresenter(this, filters); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_filter; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + FilterDto filters = Parcels.unwrap(getArguments().getParcelable(FILTERS_KEY)); + swLecturesOnly.setChecked(filters.isLecture()); + swHasAudiobook.setChecked(filters.isAudiobook()); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.menu_filter, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_accept) { + getPresenter().updateFilters(swLecturesOnly.isChecked(), + swHasAudiobook.isChecked(), + pflEpochs.getSelectedCategories(), + pflGenres.getSelectedCategories(), + pflKinds.getSelectedCategories()); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void displayEpochs(List data) { + pflEpochs.addCategories(data); + } + + @Override + public void displayGenres(List data) { + pflGenres.addCategories(data); + } + + @Override + public void displayKinds(List data) { + pflKinds.addCategories(data); + } + + @Override + public void applyFilters(FilterDto filterDto) { + if (getActivity() != null) { + Intent intent = new Intent(); + intent.putExtra(RESULT_FILTERS_KEY, Parcels.wrap(filterDto)); + + getActivity().setResult(Activity.RESULT_OK, intent); + getActivity().finish(); + } + } + + @Override + public void showEpochsError() { + Toast.makeText(getContext(), R.string.load_epochs_failed, Toast.LENGTH_SHORT).show(); + pflEpochs.showRetryButton(new FiltersProgressFlowLayout.FiltersProgressFlowLayoutRetryListener() { + @Override + public void onRetryClicked() { + getPresenter().loadEpochs(); + } + }); + } + + @Override + public void showGenresError() { + Toast.makeText(getContext(), R.string.load_genres_failed, Toast.LENGTH_SHORT).show(); + pflGenres.showRetryButton(new FiltersProgressFlowLayout.FiltersProgressFlowLayoutRetryListener() { + @Override + public void onRetryClicked() { + getPresenter().loadGenres(); + } + }); + } + + @Override + public void showKindsError() { + Toast.makeText(getContext(), R.string.load_kinds_failed, Toast.LENGTH_SHORT).show(); + pflKinds.showRetryButton(new FiltersProgressFlowLayout.FiltersProgressFlowLayoutRetryListener() { + @Override + public void onRetryClicked() { + getPresenter().loadKinds(); + } + }); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterPresenter.java new file mode 100644 index 0000000..c473578 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterPresenter.java @@ -0,0 +1,153 @@ +package com.moiseum.wolnelektury.view.search.filter; + +import android.os.Bundle; + +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.RestClient; +import com.moiseum.wolnelektury.connection.RestClientCallback; +import com.moiseum.wolnelektury.connection.models.CategoryModel; +import com.moiseum.wolnelektury.connection.services.CategoriesService; +import com.moiseum.wolnelektury.view.search.dto.FilterDto; + +import java.util.List; + +import retrofit2.Call; + +/** + * Created by piotrostrowski on 25.11.2017. + */ + +public class FilterPresenter extends FragmentPresenter { + + private FilterDto previousFilters; + private RestClient restClient; + + private Call> epochsCall; + private Call> genresCall; + private Call> kindsCall; + + FilterPresenter(FilterView view, FilterDto filters) { + super(view); + this.previousFilters = filters; + this.restClient = WLApplication.getInstance().getRestClient(); + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + loadEpochs(); + loadGenres(); + loadKinds(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (epochsCall != null) { + epochsCall.cancel(); + } + if (genresCall != null) { + genresCall.cancel(); + } + if (kindsCall != null) { + kindsCall.cancel(); + } + } + + void loadKinds() { + kindsCall = restClient.call(new RestClientCallback, CategoriesService>() { + @Override + public void onSuccess(List data) { + matchFilters(previousFilters.getFilteredKinds(), data); + getView().displayKinds(data); + } + + @Override + public void onFailure(Exception e) { + getView().showKindsError(); + } + + @Override + public void onCancel() { + // nop. + } + + @Override + public Call> execute(CategoriesService service) { + return service.getKinds(true); + } + }, CategoriesService.class); + } + + void loadGenres() { + genresCall = restClient.call(new RestClientCallback, CategoriesService>() { + @Override + public void onSuccess(List data) { + matchFilters(previousFilters.getFilteredGenres(), data); + getView().displayGenres(data); + } + + @Override + public void onFailure(Exception e) { + getView().showGenresError(); + } + + @Override + public void onCancel() { + // nop. + } + + @Override + public Call> execute(CategoriesService service) { + return service.getGenres(true); + } + }, CategoriesService.class); + } + + void loadEpochs() { + epochsCall = restClient.call(new RestClientCallback, CategoriesService>() { + @Override + public void onSuccess(List data) { + matchFilters(previousFilters.getFilteredEpochs(), data); + getView().displayEpochs(data); + } + + @Override + public void onFailure(Exception e) { + getView().showEpochsError(); + } + + @Override + public void onCancel() { + // nop. + } + + @Override + public Call> execute(CategoriesService service) { + return service.getEpochs(true); + } + }, CategoriesService.class); + } + + private void matchFilters(List previousFilters, List currentFilters) { + for (CategoryModel model : currentFilters) { + for (CategoryModel prevModel : previousFilters) { + if (model.getSlug().equals(prevModel.getSlug())) { + model.setChecked(true); + break; + } + } + } + } + + void updateFilters(boolean lecturesOnly, boolean audiobook, List epochs, List genres, List kinds) { + FilterDto dto = new FilterDto(); + dto.setLecture(lecturesOnly); + dto.setAudiobook(audiobook); + dto.setFilteredEpochs(epochs); + dto.setFilteredGenres(genres); + dto.setFilteredKinds(kinds); + getView().applyFilters(dto); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterView.java new file mode 100644 index 0000000..60ed66c --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterView.java @@ -0,0 +1,26 @@ +package com.moiseum.wolnelektury.view.search.filter; + +import com.moiseum.wolnelektury.connection.models.CategoryModel; +import com.moiseum.wolnelektury.view.search.dto.FilterDto; + +import java.util.List; + +/** + * Created by piotrostrowski on 25.11.2017. + */ + +public interface FilterView { + void displayEpochs(List data); + + void displayGenres(List data); + + void displayKinds(List data); + + void applyFilters(FilterDto filterDto); + + void showEpochsError(); + + void showGenresError(); + + void showKindsError(); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsFragment.java new file mode 100644 index 0000000..753addb --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsFragment.java @@ -0,0 +1,91 @@ +package com.moiseum.wolnelektury.view.settings; + +import android.app.ProgressDialog; +import android.os.Bundle; +import android.support.v7.widget.SwitchCompat; +import android.view.View; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.TextView; +import android.widget.Toast; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.mvp.PresenterFragment; +import com.moiseum.wolnelektury.view.supportus.SupportUsActivity; + +import butterknife.BindView; +import butterknife.OnCheckedChanged; +import butterknife.OnClick; + +public class SettingsFragment extends PresenterFragment implements SettingsView { + + public static SettingsFragment newInstance() { + return new SettingsFragment(); + } + + @BindView(R.id.swNotifications) + SwitchCompat swNotifications; + @BindView(R.id.tvState) + TextView tvState; + @BindView(R.id.btnBecomeAFriend) + Button btnBecomeAFriend; + + private ProgressDialog progressDialog; + + @Override + protected SettingsPresenter createPresenter() { + return new SettingsPresenter(this); + } + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_settings; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + } + + @OnClick(R.id.btnBecomeAFriend) + public void onBecomeAFriendClicked() { + showPayPalForm(); + } + + @OnClick(R.id.btnDelete) + public void onDeleteAllClicked() { + getPresenter().onDeleteAllClicked(); + } + + @OnCheckedChanged(R.id.swNotifications) + public void onNotificationsCheckedChanged(CompoundButton button, boolean checked) { + getPresenter().onNotificationsChanged(checked); + } + + @Override + public void initializeSettings(boolean notifications, boolean userPremium) { + swNotifications.setChecked(notifications); + tvState.setText(userPremium ? R.string.active : R.string.inactive); + btnBecomeAFriend.setVisibility(userPremium ? View.GONE : View.VISIBLE); + } + + @Override + public void showProgressDialog(boolean visible) { + if (visible && progressDialog == null) { + String dialogMessage = getString(R.string.removing_all_files); + progressDialog = ProgressDialog.show(getContext(), null, dialogMessage, true, false); + } else if (!visible && progressDialog != null) { + progressDialog.hide(); + progressDialog = null; + } + } + + @Override + public void showDeletionCompleted() { + Toast.makeText(getContext(), R.string.all_files_removed, Toast.LENGTH_SHORT).show(); + } + + @Override + public void showDeletionFailed(Throwable error) { + Toast.makeText(getContext(), getString(R.string.all_files_failed_to_remove, error.getMessage()), Toast.LENGTH_SHORT).show(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsPresenter.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsPresenter.java new file mode 100644 index 0000000..85b6f13 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsPresenter.java @@ -0,0 +1,89 @@ +package com.moiseum.wolnelektury.view.settings; + +import android.os.Bundle; + +import com.moiseum.wolnelektury.base.WLApplication; +import com.moiseum.wolnelektury.base.mvp.FragmentPresenter; +import com.moiseum.wolnelektury.connection.downloads.FileCacheUtils; +import com.moiseum.wolnelektury.connection.models.BookModel; +import com.moiseum.wolnelektury.events.LoggedInEvent; +import com.moiseum.wolnelektury.storage.BookStorage; +import com.moiseum.wolnelektury.utils.SharedPreferencesUtils; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +class SettingsPresenter extends FragmentPresenter { + + private SharedPreferencesUtils preferences = WLApplication.getInstance().getPreferences(); + private BookStorage storage = WLApplication.getInstance().getBookStorage(); + + SettingsPresenter(SettingsView view) { + super(view); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EventBus.getDefault().register(this); + } + + @Override + public void onViewCreated(Bundle savedInstanceState) { + super.onViewCreated(savedInstanceState); + getView().initializeSettings(preferences.getNotifications(), preferences.isUserPremium()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + EventBus.getDefault().unregister(this); + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onLoggedIn(LoggedInEvent event) { + getView().initializeSettings(preferences.getNotifications(), preferences.isUserPremium()); + } + + public void onNotificationsChanged(boolean checked) { + preferences.setNotifications(checked); + } + + public void onDeleteAllClicked() { + getView().showProgressDialog(true); + addDisposable(deleteAllFiles() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + getView().showProgressDialog(false); + getView().showDeletionCompleted(); + }, error -> { + getView().showProgressDialog(false); + getView().showDeletionFailed(error); + }) + ); + } + + private Completable deleteAllFiles() { + return Completable.fromAction(() -> { + List storedBooks = storage.all(); + for (BookModel book : storedBooks) { + if (book.getEbookFileUrl() != null) { + FileCacheUtils.deleteEbookFile(book.getEbookFileUrl()); + } + if (book.getAudioFileUrls() != null) { + FileCacheUtils.deleteAudiobookFiles(book.getAudioFileUrls()); + } + } + storage.removeAll(); + }); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsView.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsView.java new file mode 100644 index 0000000..461cfa7 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsView.java @@ -0,0 +1,12 @@ +package com.moiseum.wolnelektury.view.settings; + +public interface SettingsView { + + void initializeSettings(boolean notifications, boolean userPremium); + + void showProgressDialog(boolean visible); + + void showDeletionCompleted(); + + void showDeletionFailed(Throwable error); +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/splash/SplashActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/splash/SplashActivity.java new file mode 100644 index 0000000..1088196 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/splash/SplashActivity.java @@ -0,0 +1,58 @@ +package com.moiseum.wolnelektury.view.splash; + +import android.os.Bundle; +import android.os.Handler; +import android.view.View; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.view.main.MainActivity; + +import butterknife.BindView; + +/** + * Created by piotrostrowski on 09.12.2017. + */ + +public class SplashActivity extends AbstractActivity { + + @BindView(R.id.rlMainView) + View rlMainView; + + private Handler launchHandler; + private Runnable launchRunnable = new Runnable() { + @Override + public void run() { + launchDashboard(); + } + }; + + @Override + public int getLayoutResourceId() { + return R.layout.activity_splash; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + rlMainView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + launchDashboard(); + } + }); + launchHandler = new Handler(); + launchHandler.postDelayed(launchRunnable, 1500); + } + + @Override + public void onDestroy() { + super.onDestroy(); + launchHandler.removeCallbacks(launchRunnable); + } + + private void launchDashboard() { + MainActivity.MainIntent intent = new MainActivity.MainIntent(this); + startActivity(intent); + finish(); + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/supportus/SupportUsActivity.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/supportus/SupportUsActivity.java new file mode 100644 index 0000000..c8fd191 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/supportus/SupportUsActivity.java @@ -0,0 +1,39 @@ +package com.moiseum.wolnelektury.view.supportus; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractActivity; +import com.moiseum.wolnelektury.base.AbstractIntent; + +/** + * @author golonkos + */ + +public class SupportUsActivity extends AbstractActivity { + + public static class SupportUsIntent extends AbstractIntent { + + public SupportUsIntent(Context packageContext) { + super(packageContext, SupportUsActivity.class); + } + } + + @Override + public int getLayoutResourceId() { + return R.layout.activity_blank; + } + + @Override + public void prepareView(Bundle savedInstanceState) { + setBackButtonEnable(true); + setTitle(R.string.support_us); + + if (savedInstanceState == null) { + Fragment fragment = SupportUsFragment.newInstance(); + getSupportFragmentManager().beginTransaction().add(R.id.flContainer, fragment).commit(); + } + } +} diff --git a/Android/app/src/main/java/com/moiseum/wolnelektury/view/supportus/SupportUsFragment.java b/Android/app/src/main/java/com/moiseum/wolnelektury/view/supportus/SupportUsFragment.java new file mode 100644 index 0000000..9cafd08 --- /dev/null +++ b/Android/app/src/main/java/com/moiseum/wolnelektury/view/supportus/SupportUsFragment.java @@ -0,0 +1,38 @@ +package com.moiseum.wolnelektury.view.supportus; + +import android.os.Bundle; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.View; +import android.widget.TextView; + +import com.moiseum.wolnelektury.R; +import com.moiseum.wolnelektury.base.AbstractFragment; + +import butterknife.BindView; + +/** + * @author golonkos + */ + +public class SupportUsFragment extends AbstractFragment { + + public static SupportUsFragment newInstance() { + return new SupportUsFragment(); + } + + @BindView(R.id.tvSupportUsText) + TextView tvSupportUsText; + + @Override + public int getLayoutResourceId() { + return R.layout.fragment_support_us; + } + + @Override + public void prepareView(View view, Bundle savedInstanceState) { + tvSupportUsText.setText(Html.fromHtml(getString(R.string.support_us_text))); + tvSupportUsText.setLinksClickable(true); + tvSupportUsText.setMovementMethod(LinkMovementMethod.getInstance()); + } +} diff --git a/Android/app/src/main/res/color/selector_button_white_border_text_color.xml b/Android/app/src/main/res/color/selector_button_white_border_text_color.xml new file mode 100644 index 0000000..70e59d9 --- /dev/null +++ b/Android/app/src/main/res/color/selector_button_white_border_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable-hdpi/font_big.png b/Android/app/src/main/res/drawable-hdpi/font_big.png new file mode 100644 index 0000000..20ef47e Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/font_big.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/font_small.png b/Android/app/src/main/res/drawable-hdpi/font_small.png new file mode 100644 index 0000000..ccf030e Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/font_small.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_about.png b/Android/app/src/main/res/drawable-hdpi/ic_about.png new file mode 100644 index 0000000..f93ffd8 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_about.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_accept.png b/Android/app/src/main/res/drawable-hdpi/ic_accept.png new file mode 100644 index 0000000..2a56e3a Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_accept.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_accept_new.png b/Android/app/src/main/res/drawable-hdpi/ic_accept_new.png new file mode 100644 index 0000000..e105b16 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_accept_new.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_book.png b/Android/app/src/main/res/drawable-hdpi/ic_book.png new file mode 100644 index 0000000..4a8de9c Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_book.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_close.png b/Android/app/src/main/res/drawable-hdpi/ic_close.png new file mode 100644 index 0000000..ca46f8d Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_close.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_close_small.png b/Android/app/src/main/res/drawable-hdpi/ic_close_small.png new file mode 100644 index 0000000..ed66f04 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_close_small.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_comment.png b/Android/app/src/main/res/drawable-hdpi/ic_comment.png new file mode 100644 index 0000000..25a86ea Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_comment.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_fav.png b/Android/app/src/main/res/drawable-hdpi/ic_fav.png new file mode 100644 index 0000000..6cf4b63 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_fav.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_fav_active.png b/Android/app/src/main/res/drawable-hdpi/ic_fav_active.png new file mode 100644 index 0000000..c45bad8 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_fav_active.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_fav_big.png b/Android/app/src/main/res/drawable-hdpi/ic_fav_big.png new file mode 100644 index 0000000..70f71bc Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_fav_big.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_fav_big_active.png b/Android/app/src/main/res/drawable-hdpi/ic_fav_big_active.png new file mode 100644 index 0000000..85dd411 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_fav_big_active.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_filter.png b/Android/app/src/main/res/drawable-hdpi/ic_filter.png new file mode 100644 index 0000000..b54cd72 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_filter.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_filter_new.png b/Android/app/src/main/res/drawable-hdpi/ic_filter_new.png new file mode 100644 index 0000000..3db46e7 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_filter_new.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_glass_big.png b/Android/app/src/main/res/drawable-hdpi/ic_glass_big.png new file mode 100644 index 0000000..6f7f7d4 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_glass_big.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_glass_mid.png b/Android/app/src/main/res/drawable-hdpi/ic_glass_mid.png new file mode 100644 index 0000000..02148cd Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_glass_mid.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_glass_small.png b/Android/app/src/main/res/drawable-hdpi/ic_glass_small.png new file mode 100644 index 0000000..078030e Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_glass_small.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_menu_all.png b/Android/app/src/main/res/drawable-hdpi/ic_menu_all.png new file mode 100644 index 0000000..cb8c33e Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_menu_all.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_menu_audiobook.png b/Android/app/src/main/res/drawable-hdpi/ic_menu_audiobook.png new file mode 100644 index 0000000..0f3673a Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_menu_audiobook.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_menu_downloaded.png b/Android/app/src/main/res/drawable-hdpi/ic_menu_downloaded.png new file mode 100644 index 0000000..7083d61 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_menu_downloaded.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_menu_fav.png b/Android/app/src/main/res/drawable-hdpi/ic_menu_fav.png new file mode 100644 index 0000000..4a765de Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_menu_fav.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_menu_library.png b/Android/app/src/main/res/drawable-hdpi/ic_menu_library.png new file mode 100644 index 0000000..8190365 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_menu_library.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_menu_new.png b/Android/app/src/main/res/drawable-hdpi/ic_menu_new.png new file mode 100644 index 0000000..026368a Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_menu_new.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_menu_search.png b/Android/app/src/main/res/drawable-hdpi/ic_menu_search.png new file mode 100644 index 0000000..6938a57 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_menu_search.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_menu_star.png b/Android/app/src/main/res/drawable-hdpi/ic_menu_star.png new file mode 100644 index 0000000..d55c098 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_menu_star.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_news.png b/Android/app/src/main/res/drawable-hdpi/ic_news.png new file mode 100644 index 0000000..7f9be27 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_news.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_next.png b/Android/app/src/main/res/drawable-hdpi/ic_next.png new file mode 100644 index 0000000..b749b24 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_next.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_notification.png b/Android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100755 index 0000000..88b72be Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_notification_player.png b/Android/app/src/main/res/drawable-hdpi/ic_notification_player.png new file mode 100755 index 0000000..36707d5 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_notification_player.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png b/Android/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png new file mode 100755 index 0000000..f7b3c1e Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png b/Android/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png new file mode 100755 index 0000000..be73548 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_prev.png b/Android/app/src/main/res/drawable-hdpi/ic_prev.png new file mode 100644 index 0000000..48e76de Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_prev.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_reader_dark.png b/Android/app/src/main/res/drawable-hdpi/ic_reader_dark.png new file mode 100644 index 0000000..23b5264 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_reader_dark.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_reader_light.png b/Android/app/src/main/res/drawable-hdpi/ic_reader_light.png new file mode 100644 index 0000000..4909c04 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_reader_light.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_reload.png b/Android/app/src/main/res/drawable-hdpi/ic_reload.png new file mode 100644 index 0000000..bc2168c Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_reload.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_search.png b/Android/app/src/main/res/drawable-hdpi/ic_search.png new file mode 100644 index 0000000..e5c549d Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_search.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_search_new.png b/Android/app/src/main/res/drawable-hdpi/ic_search_new.png new file mode 100644 index 0000000..1f25cb7 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_search_new.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_settings.png b/Android/app/src/main/res/drawable-hdpi/ic_settings.png new file mode 100644 index 0000000..38d8efc Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_settings.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_share.png b/Android/app/src/main/res/drawable-hdpi/ic_share.png new file mode 100644 index 0000000..3e7605d Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_share.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_skip_next_white_24dp.png b/Android/app/src/main/res/drawable-hdpi/ic_skip_next_white_24dp.png new file mode 100755 index 0000000..3c6ae11 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_skip_next_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_skip_previous_white_24dp.png b/Android/app/src/main/res/drawable-hdpi/ic_skip_previous_white_24dp.png new file mode 100755 index 0000000..6883e61 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_skip_previous_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_speaker_big.png b/Android/app/src/main/res/drawable-hdpi/ic_speaker_big.png new file mode 100644 index 0000000..59abb5d Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_speaker_big.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_speaker_mid.png b/Android/app/src/main/res/drawable-hdpi/ic_speaker_mid.png new file mode 100644 index 0000000..0c38e82 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_speaker_mid.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_speaker_small.png b/Android/app/src/main/res/drawable-hdpi/ic_speaker_small.png new file mode 100644 index 0000000..e90266e Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_speaker_small.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_star_big_active.png b/Android/app/src/main/res/drawable-hdpi/ic_star_big_active.png new file mode 100644 index 0000000..74ded13 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_star_big_active.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_star_mid.png b/Android/app/src/main/res/drawable-hdpi/ic_star_mid.png new file mode 100644 index 0000000..522848a Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_star_mid.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_star_small_selected.png b/Android/app/src/main/res/drawable-hdpi/ic_star_small_selected.png new file mode 100644 index 0000000..86f931a Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_star_small_selected.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_stat_image_audiotrack.png b/Android/app/src/main/res/drawable-hdpi/ic_stat_image_audiotrack.png new file mode 100755 index 0000000..bbbaa0e Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_stat_image_audiotrack.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_toggle.png b/Android/app/src/main/res/drawable-hdpi/ic_toggle.png new file mode 100644 index 0000000..4ade18b Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_toggle.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/ic_trash.png b/Android/app/src/main/res/drawable-hdpi/ic_trash.png new file mode 100644 index 0000000..e0801be Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/ic_trash.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/logo_fnp.png b/Android/app/src/main/res/drawable-hdpi/logo_fnp.png new file mode 100644 index 0000000..55313a5 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/logo_fnp.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/logo_mkidn.png b/Android/app/src/main/res/drawable-hdpi/logo_mkidn.png new file mode 100644 index 0000000..27e286b Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/logo_mkidn.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/logo_opp.png b/Android/app/src/main/res/drawable-hdpi/logo_opp.png new file mode 100644 index 0000000..f790721 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/logo_opp.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/logo_wl_light.png b/Android/app/src/main/res/drawable-hdpi/logo_wl_light.png new file mode 100644 index 0000000..464f5ea Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/logo_wl_light.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/player_chapter_next.png b/Android/app/src/main/res/drawable-hdpi/player_chapter_next.png new file mode 100644 index 0000000..f557ce6 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/player_chapter_next.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/player_chapter_previous.png b/Android/app/src/main/res/drawable-hdpi/player_chapter_previous.png new file mode 100644 index 0000000..32939f4 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/player_chapter_previous.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/player_controls_forward.png b/Android/app/src/main/res/drawable-hdpi/player_controls_forward.png new file mode 100644 index 0000000..e504fc6 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/player_controls_forward.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/player_controls_pause.png b/Android/app/src/main/res/drawable-hdpi/player_controls_pause.png new file mode 100644 index 0000000..424785f Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/player_controls_pause.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/player_controls_play.png b/Android/app/src/main/res/drawable-hdpi/player_controls_play.png new file mode 100644 index 0000000..39e1775 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/player_controls_play.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/player_controls_rewind.png b/Android/app/src/main/res/drawable-hdpi/player_controls_rewind.png new file mode 100644 index 0000000..84ed38b Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/player_controls_rewind.png differ diff --git a/Android/app/src/main/res/drawable-hdpi/splash.png b/Android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000..78ca297 Binary files /dev/null and b/Android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/font_big.png b/Android/app/src/main/res/drawable-mdpi/font_big.png new file mode 100644 index 0000000..1d3b7ba Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/font_big.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/font_small.png b/Android/app/src/main/res/drawable-mdpi/font_small.png new file mode 100644 index 0000000..e4e952d Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/font_small.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_about.png b/Android/app/src/main/res/drawable-mdpi/ic_about.png new file mode 100644 index 0000000..74cebd2 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_about.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_accept.png b/Android/app/src/main/res/drawable-mdpi/ic_accept.png new file mode 100644 index 0000000..be0bbcf Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_accept.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_accept_new.png b/Android/app/src/main/res/drawable-mdpi/ic_accept_new.png new file mode 100644 index 0000000..bea2e31 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_accept_new.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_book.png b/Android/app/src/main/res/drawable-mdpi/ic_book.png new file mode 100644 index 0000000..e034e68 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_book.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_close.png b/Android/app/src/main/res/drawable-mdpi/ic_close.png new file mode 100644 index 0000000..0a29e5e Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_close.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_close_small.png b/Android/app/src/main/res/drawable-mdpi/ic_close_small.png new file mode 100644 index 0000000..b43690f Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_close_small.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_comment.png b/Android/app/src/main/res/drawable-mdpi/ic_comment.png new file mode 100644 index 0000000..6db715f Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_comment.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_fav.png b/Android/app/src/main/res/drawable-mdpi/ic_fav.png new file mode 100644 index 0000000..e192f17 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_fav.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_fav_active.png b/Android/app/src/main/res/drawable-mdpi/ic_fav_active.png new file mode 100644 index 0000000..d902010 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_fav_active.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_fav_big.png b/Android/app/src/main/res/drawable-mdpi/ic_fav_big.png new file mode 100644 index 0000000..63b5fef Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_fav_big.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_fav_big_active.png b/Android/app/src/main/res/drawable-mdpi/ic_fav_big_active.png new file mode 100644 index 0000000..a32b84c Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_fav_big_active.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_filter.png b/Android/app/src/main/res/drawable-mdpi/ic_filter.png new file mode 100644 index 0000000..235bb40 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_filter.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_filter_new.png b/Android/app/src/main/res/drawable-mdpi/ic_filter_new.png new file mode 100644 index 0000000..f049b15 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_filter_new.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_glass_big.png b/Android/app/src/main/res/drawable-mdpi/ic_glass_big.png new file mode 100644 index 0000000..0339233 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_glass_big.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_glass_mid.png b/Android/app/src/main/res/drawable-mdpi/ic_glass_mid.png new file mode 100644 index 0000000..d121b26 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_glass_mid.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_glass_small.png b/Android/app/src/main/res/drawable-mdpi/ic_glass_small.png new file mode 100644 index 0000000..d7a0e70 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_glass_small.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_menu_all.png b/Android/app/src/main/res/drawable-mdpi/ic_menu_all.png new file mode 100644 index 0000000..cb687a8 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_menu_all.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_menu_audiobook.png b/Android/app/src/main/res/drawable-mdpi/ic_menu_audiobook.png new file mode 100644 index 0000000..5e531ac Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_menu_audiobook.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_menu_fav.png b/Android/app/src/main/res/drawable-mdpi/ic_menu_fav.png new file mode 100644 index 0000000..d27d7fb Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_menu_fav.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_menu_new.png b/Android/app/src/main/res/drawable-mdpi/ic_menu_new.png new file mode 100644 index 0000000..2083c02 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_menu_new.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_menu_search.png b/Android/app/src/main/res/drawable-mdpi/ic_menu_search.png new file mode 100644 index 0000000..cceabf0 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_menu_search.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_menu_star.png b/Android/app/src/main/res/drawable-mdpi/ic_menu_star.png new file mode 100644 index 0000000..0dada46 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_menu_star.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_news.png b/Android/app/src/main/res/drawable-mdpi/ic_news.png new file mode 100644 index 0000000..9a71adc Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_news.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_next.png b/Android/app/src/main/res/drawable-mdpi/ic_next.png new file mode 100644 index 0000000..858d479 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_next.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_notification.png b/Android/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100755 index 0000000..fea1063 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_notification_player.png b/Android/app/src/main/res/drawable-mdpi/ic_notification_player.png new file mode 100755 index 0000000..18dad79 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_notification_player.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png b/Android/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png new file mode 100755 index 0000000..499341f Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png b/Android/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png new file mode 100755 index 0000000..bebdf37 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_prev.png b/Android/app/src/main/res/drawable-mdpi/ic_prev.png new file mode 100644 index 0000000..8a91462 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_prev.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_reader_dark.png b/Android/app/src/main/res/drawable-mdpi/ic_reader_dark.png new file mode 100644 index 0000000..da720bd Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_reader_dark.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_reader_light.png b/Android/app/src/main/res/drawable-mdpi/ic_reader_light.png new file mode 100644 index 0000000..7eef3a2 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_reader_light.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_reload.png b/Android/app/src/main/res/drawable-mdpi/ic_reload.png new file mode 100644 index 0000000..69a526f Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_reload.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_search.png b/Android/app/src/main/res/drawable-mdpi/ic_search.png new file mode 100644 index 0000000..376356d Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_search.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_search_new.png b/Android/app/src/main/res/drawable-mdpi/ic_search_new.png new file mode 100644 index 0000000..e68eac6 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_search_new.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_settings.png b/Android/app/src/main/res/drawable-mdpi/ic_settings.png new file mode 100644 index 0000000..45f023c Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_settings.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_share.png b/Android/app/src/main/res/drawable-mdpi/ic_share.png new file mode 100644 index 0000000..7bd86ca Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_share.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_skip_next_white_24dp.png b/Android/app/src/main/res/drawable-mdpi/ic_skip_next_white_24dp.png new file mode 100755 index 0000000..d55f7e6 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_skip_next_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_skip_previous_white_24dp.png b/Android/app/src/main/res/drawable-mdpi/ic_skip_previous_white_24dp.png new file mode 100755 index 0000000..259797d Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_skip_previous_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_speaker_big.png b/Android/app/src/main/res/drawable-mdpi/ic_speaker_big.png new file mode 100644 index 0000000..e8edcc1 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_speaker_big.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_speaker_mid.png b/Android/app/src/main/res/drawable-mdpi/ic_speaker_mid.png new file mode 100644 index 0000000..3b818a9 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_speaker_mid.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_speaker_small.png b/Android/app/src/main/res/drawable-mdpi/ic_speaker_small.png new file mode 100644 index 0000000..cdeef41 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_speaker_small.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_star_big_active.png b/Android/app/src/main/res/drawable-mdpi/ic_star_big_active.png new file mode 100644 index 0000000..5d5d7e4 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_star_big_active.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_star_mid.png b/Android/app/src/main/res/drawable-mdpi/ic_star_mid.png new file mode 100644 index 0000000..142ef31 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_star_mid.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_star_small_selected.png b/Android/app/src/main/res/drawable-mdpi/ic_star_small_selected.png new file mode 100644 index 0000000..4f37c4c Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_star_small_selected.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_stat_image_audiotrack.png b/Android/app/src/main/res/drawable-mdpi/ic_stat_image_audiotrack.png new file mode 100755 index 0000000..e445a0c Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_stat_image_audiotrack.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_toggle.png b/Android/app/src/main/res/drawable-mdpi/ic_toggle.png new file mode 100644 index 0000000..b77af65 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_toggle.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/ic_trash.png b/Android/app/src/main/res/drawable-mdpi/ic_trash.png new file mode 100644 index 0000000..4ef2614 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/ic_trash.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/logo_fnp.png b/Android/app/src/main/res/drawable-mdpi/logo_fnp.png new file mode 100644 index 0000000..bc54c4b Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/logo_fnp.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/logo_mkidn.png b/Android/app/src/main/res/drawable-mdpi/logo_mkidn.png new file mode 100644 index 0000000..7251057 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/logo_mkidn.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/logo_opp.png b/Android/app/src/main/res/drawable-mdpi/logo_opp.png new file mode 100644 index 0000000..f0ca31f Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/logo_opp.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/logo_wl_light.png b/Android/app/src/main/res/drawable-mdpi/logo_wl_light.png new file mode 100644 index 0000000..81f2299 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/logo_wl_light.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/player_chapter_next.png b/Android/app/src/main/res/drawable-mdpi/player_chapter_next.png new file mode 100644 index 0000000..6780db8 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/player_chapter_next.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/player_chapter_previous.png b/Android/app/src/main/res/drawable-mdpi/player_chapter_previous.png new file mode 100644 index 0000000..fc34846 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/player_chapter_previous.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/player_controls_forward.png b/Android/app/src/main/res/drawable-mdpi/player_controls_forward.png new file mode 100644 index 0000000..e478c81 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/player_controls_forward.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/player_controls_pause.png b/Android/app/src/main/res/drawable-mdpi/player_controls_pause.png new file mode 100644 index 0000000..7d97409 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/player_controls_pause.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/player_controls_play.png b/Android/app/src/main/res/drawable-mdpi/player_controls_play.png new file mode 100644 index 0000000..b6aadde Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/player_controls_play.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/player_controls_rewind.png b/Android/app/src/main/res/drawable-mdpi/player_controls_rewind.png new file mode 100644 index 0000000..0316d9b Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/player_controls_rewind.png differ diff --git a/Android/app/src/main/res/drawable-mdpi/splash.png b/Android/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 0000000..f31aa10 Binary files /dev/null and b/Android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/Android/app/src/main/res/drawable-nodpi/album_jazz_blues.jpg b/Android/app/src/main/res/drawable-nodpi/album_jazz_blues.jpg new file mode 100755 index 0000000..d3ffefc Binary files /dev/null and b/Android/app/src/main/res/drawable-nodpi/album_jazz_blues.jpg differ diff --git a/Android/app/src/main/res/drawable-nodpi/album_youtube_audio_library_rock_2.jpg b/Android/app/src/main/res/drawable-nodpi/album_youtube_audio_library_rock_2.jpg new file mode 100755 index 0000000..8d5020c Binary files /dev/null and b/Android/app/src/main/res/drawable-nodpi/album_youtube_audio_library_rock_2.jpg differ diff --git a/Android/app/src/main/res/drawable-nodpi/list_nocover.png b/Android/app/src/main/res/drawable-nodpi/list_nocover.png new file mode 100644 index 0000000..c060f36 Binary files /dev/null and b/Android/app/src/main/res/drawable-nodpi/list_nocover.png differ diff --git a/Android/app/src/main/res/drawable-v21/nav_item_background_selector.xml b/Android/app/src/main/res/drawable-v21/nav_item_background_selector.xml new file mode 100644 index 0000000..f77d3af --- /dev/null +++ b/Android/app/src/main/res/drawable-v21/nav_item_background_selector.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..3bb4cdb --- /dev/null +++ b/Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/Android/app/src/main/res/drawable-xhdpi/font_big.png b/Android/app/src/main/res/drawable-xhdpi/font_big.png new file mode 100644 index 0000000..73c7c65 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/font_big.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/font_small.png b/Android/app/src/main/res/drawable-xhdpi/font_small.png new file mode 100644 index 0000000..aa12cc3 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/font_small.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_about.png b/Android/app/src/main/res/drawable-xhdpi/ic_about.png new file mode 100644 index 0000000..57d9945 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_about.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_accept.png b/Android/app/src/main/res/drawable-xhdpi/ic_accept.png new file mode 100644 index 0000000..2385af8 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_accept.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_accept_new.png b/Android/app/src/main/res/drawable-xhdpi/ic_accept_new.png new file mode 100644 index 0000000..da301bf Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_accept_new.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_book.png b/Android/app/src/main/res/drawable-xhdpi/ic_book.png new file mode 100644 index 0000000..f254579 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_book.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_close.png b/Android/app/src/main/res/drawable-xhdpi/ic_close.png new file mode 100644 index 0000000..73d2bc2 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_close.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_close_small.png b/Android/app/src/main/res/drawable-xhdpi/ic_close_small.png new file mode 100644 index 0000000..a50c710 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_close_small.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_comment.png b/Android/app/src/main/res/drawable-xhdpi/ic_comment.png new file mode 100644 index 0000000..d2a60c0 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_comment.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_fav.png b/Android/app/src/main/res/drawable-xhdpi/ic_fav.png new file mode 100644 index 0000000..2396e62 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_fav.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_fav_active.png b/Android/app/src/main/res/drawable-xhdpi/ic_fav_active.png new file mode 100644 index 0000000..e56e64d Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_fav_active.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_fav_big.png b/Android/app/src/main/res/drawable-xhdpi/ic_fav_big.png new file mode 100644 index 0000000..f5bad70 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_fav_big.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_fav_big_active.png b/Android/app/src/main/res/drawable-xhdpi/ic_fav_big_active.png new file mode 100644 index 0000000..7f1bd76 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_fav_big_active.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_filter.png b/Android/app/src/main/res/drawable-xhdpi/ic_filter.png new file mode 100644 index 0000000..f60550e Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_filter.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_filter_new.png b/Android/app/src/main/res/drawable-xhdpi/ic_filter_new.png new file mode 100644 index 0000000..80f1aff Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_filter_new.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_glass_big.png b/Android/app/src/main/res/drawable-xhdpi/ic_glass_big.png new file mode 100644 index 0000000..17dbc1e Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_glass_big.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_glass_mid.png b/Android/app/src/main/res/drawable-xhdpi/ic_glass_mid.png new file mode 100644 index 0000000..133799e Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_glass_mid.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_glass_small.png b/Android/app/src/main/res/drawable-xhdpi/ic_glass_small.png new file mode 100644 index 0000000..8b48329 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_glass_small.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_menu_all.png b/Android/app/src/main/res/drawable-xhdpi/ic_menu_all.png new file mode 100644 index 0000000..3cf4ac0 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_menu_all.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_menu_audiobook.png b/Android/app/src/main/res/drawable-xhdpi/ic_menu_audiobook.png new file mode 100644 index 0000000..a867029 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_menu_audiobook.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_menu_downloaded.png b/Android/app/src/main/res/drawable-xhdpi/ic_menu_downloaded.png new file mode 100644 index 0000000..50c281b Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_menu_downloaded.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_menu_fav.png b/Android/app/src/main/res/drawable-xhdpi/ic_menu_fav.png new file mode 100644 index 0000000..41f4f33 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_menu_fav.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_menu_library.png b/Android/app/src/main/res/drawable-xhdpi/ic_menu_library.png new file mode 100644 index 0000000..3dd5128 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_menu_library.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_menu_new.png b/Android/app/src/main/res/drawable-xhdpi/ic_menu_new.png new file mode 100644 index 0000000..e64fdfd Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_menu_new.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_menu_search.png b/Android/app/src/main/res/drawable-xhdpi/ic_menu_search.png new file mode 100644 index 0000000..c84577b Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_menu_search.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_menu_star.png b/Android/app/src/main/res/drawable-xhdpi/ic_menu_star.png new file mode 100644 index 0000000..1ac0cc3 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_menu_star.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_news.png b/Android/app/src/main/res/drawable-xhdpi/ic_news.png new file mode 100644 index 0000000..a6d35ec Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_news.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_next.png b/Android/app/src/main/res/drawable-xhdpi/ic_next.png new file mode 100644 index 0000000..e12c4a3 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_next.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_notification.png b/Android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100755 index 0000000..639f40b Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_notification_player.png b/Android/app/src/main/res/drawable-xhdpi/ic_notification_player.png new file mode 100755 index 0000000..c0a5a5f Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_notification_player.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png b/Android/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png new file mode 100755 index 0000000..e31b5b0 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png b/Android/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png new file mode 100755 index 0000000..929ffec Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_prev.png b/Android/app/src/main/res/drawable-xhdpi/ic_prev.png new file mode 100644 index 0000000..bbc95b9 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_prev.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_reader_dark.png b/Android/app/src/main/res/drawable-xhdpi/ic_reader_dark.png new file mode 100644 index 0000000..84ed776 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_reader_dark.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_reader_light.png b/Android/app/src/main/res/drawable-xhdpi/ic_reader_light.png new file mode 100644 index 0000000..498d30c Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_reader_light.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_reload.png b/Android/app/src/main/res/drawable-xhdpi/ic_reload.png new file mode 100644 index 0000000..7f2a9ea Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_reload.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_search.png b/Android/app/src/main/res/drawable-xhdpi/ic_search.png new file mode 100644 index 0000000..324e394 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_search.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_search_new.png b/Android/app/src/main/res/drawable-xhdpi/ic_search_new.png new file mode 100644 index 0000000..5beafc1 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_search_new.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_settings.png b/Android/app/src/main/res/drawable-xhdpi/ic_settings.png new file mode 100644 index 0000000..cb9a755 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_settings.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_share.png b/Android/app/src/main/res/drawable-xhdpi/ic_share.png new file mode 100644 index 0000000..62f120a Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_share.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png b/Android/app/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png new file mode 100755 index 0000000..343b7bb Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png b/Android/app/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png new file mode 100755 index 0000000..e926c5e Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_speaker_big.png b/Android/app/src/main/res/drawable-xhdpi/ic_speaker_big.png new file mode 100644 index 0000000..70c11e0 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_speaker_big.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_speaker_mid.png b/Android/app/src/main/res/drawable-xhdpi/ic_speaker_mid.png new file mode 100644 index 0000000..ecb5bef Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_speaker_mid.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_speaker_small.png b/Android/app/src/main/res/drawable-xhdpi/ic_speaker_small.png new file mode 100644 index 0000000..a512d6e Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_speaker_small.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_star_big_active.png b/Android/app/src/main/res/drawable-xhdpi/ic_star_big_active.png new file mode 100644 index 0000000..6f83624 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_star_big_active.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_star_mid.png b/Android/app/src/main/res/drawable-xhdpi/ic_star_mid.png new file mode 100644 index 0000000..bcaa804 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_star_mid.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_star_small_selected.png b/Android/app/src/main/res/drawable-xhdpi/ic_star_small_selected.png new file mode 100644 index 0000000..00c996b Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_star_small_selected.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_stat_image_audiotrack.png b/Android/app/src/main/res/drawable-xhdpi/ic_stat_image_audiotrack.png new file mode 100755 index 0000000..5481659 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_stat_image_audiotrack.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_toggle.png b/Android/app/src/main/res/drawable-xhdpi/ic_toggle.png new file mode 100644 index 0000000..ba208cd Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_toggle.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/ic_trash.png b/Android/app/src/main/res/drawable-xhdpi/ic_trash.png new file mode 100644 index 0000000..43ea464 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/ic_trash.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/logo_fnp.png b/Android/app/src/main/res/drawable-xhdpi/logo_fnp.png new file mode 100644 index 0000000..79f1164 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/logo_fnp.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/logo_mkidn.png b/Android/app/src/main/res/drawable-xhdpi/logo_mkidn.png new file mode 100644 index 0000000..ab80226 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/logo_mkidn.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/logo_opp.png b/Android/app/src/main/res/drawable-xhdpi/logo_opp.png new file mode 100644 index 0000000..e76600b Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/logo_opp.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/logo_wl_light.png b/Android/app/src/main/res/drawable-xhdpi/logo_wl_light.png new file mode 100644 index 0000000..b5c8322 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/logo_wl_light.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/player_chapter_next.png b/Android/app/src/main/res/drawable-xhdpi/player_chapter_next.png new file mode 100644 index 0000000..a9efcf7 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/player_chapter_next.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/player_chapter_previous.png b/Android/app/src/main/res/drawable-xhdpi/player_chapter_previous.png new file mode 100644 index 0000000..75d2ad5 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/player_chapter_previous.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/player_controls_forward.png b/Android/app/src/main/res/drawable-xhdpi/player_controls_forward.png new file mode 100644 index 0000000..9953f41 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/player_controls_forward.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/player_controls_pause.png b/Android/app/src/main/res/drawable-xhdpi/player_controls_pause.png new file mode 100644 index 0000000..a69143a Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/player_controls_pause.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/player_controls_play.png b/Android/app/src/main/res/drawable-xhdpi/player_controls_play.png new file mode 100644 index 0000000..b81d27f Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/player_controls_play.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/player_controls_rewind.png b/Android/app/src/main/res/drawable-xhdpi/player_controls_rewind.png new file mode 100644 index 0000000..d860315 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/player_controls_rewind.png differ diff --git a/Android/app/src/main/res/drawable-xhdpi/splash.png b/Android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000..9670ea6 Binary files /dev/null and b/Android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/font_big.png b/Android/app/src/main/res/drawable-xxhdpi/font_big.png new file mode 100644 index 0000000..6db14c5 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/font_big.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/font_small.png b/Android/app/src/main/res/drawable-xxhdpi/font_small.png new file mode 100644 index 0000000..864478a Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/font_small.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_about.png b/Android/app/src/main/res/drawable-xxhdpi/ic_about.png new file mode 100644 index 0000000..94e3a2b Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_about.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_accept.png b/Android/app/src/main/res/drawable-xxhdpi/ic_accept.png new file mode 100644 index 0000000..d998731 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_accept.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_accept_new.png b/Android/app/src/main/res/drawable-xxhdpi/ic_accept_new.png new file mode 100644 index 0000000..527eb35 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_accept_new.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_book.png b/Android/app/src/main/res/drawable-xxhdpi/ic_book.png new file mode 100644 index 0000000..56f028f Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_book.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_close.png b/Android/app/src/main/res/drawable-xxhdpi/ic_close.png new file mode 100644 index 0000000..5a5dae6 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_close.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_close_small.png b/Android/app/src/main/res/drawable-xxhdpi/ic_close_small.png new file mode 100644 index 0000000..88a40e8 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_close_small.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_comment.png b/Android/app/src/main/res/drawable-xxhdpi/ic_comment.png new file mode 100644 index 0000000..e34d5df Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_comment.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_fav.png b/Android/app/src/main/res/drawable-xxhdpi/ic_fav.png new file mode 100644 index 0000000..a104fae Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_fav.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_fav_active.png b/Android/app/src/main/res/drawable-xxhdpi/ic_fav_active.png new file mode 100644 index 0000000..ccaf300 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_fav_active.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_fav_big.png b/Android/app/src/main/res/drawable-xxhdpi/ic_fav_big.png new file mode 100644 index 0000000..614379d Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_fav_big.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_fav_big_active.png b/Android/app/src/main/res/drawable-xxhdpi/ic_fav_big_active.png new file mode 100644 index 0000000..ea2812d Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_fav_big_active.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_filter.png b/Android/app/src/main/res/drawable-xxhdpi/ic_filter.png new file mode 100644 index 0000000..b8f4dc2 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_filter.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_filter_new.png b/Android/app/src/main/res/drawable-xxhdpi/ic_filter_new.png new file mode 100644 index 0000000..2db8b8c Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_filter_new.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_glass_big.png b/Android/app/src/main/res/drawable-xxhdpi/ic_glass_big.png new file mode 100644 index 0000000..604e095 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_glass_big.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_glass_mid.png b/Android/app/src/main/res/drawable-xxhdpi/ic_glass_mid.png new file mode 100644 index 0000000..7682b03 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_glass_mid.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_glass_small.png b/Android/app/src/main/res/drawable-xxhdpi/ic_glass_small.png new file mode 100644 index 0000000..9aa59b1 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_glass_small.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_media_with_pause.png b/Android/app/src/main/res/drawable-xxhdpi/ic_media_with_pause.png new file mode 100755 index 0000000..c6041da Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_media_with_pause.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_media_with_play.png b/Android/app/src/main/res/drawable-xxhdpi/ic_media_with_play.png new file mode 100755 index 0000000..a0da57b Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_media_with_play.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_menu_all.png b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_all.png new file mode 100644 index 0000000..dbe65df Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_all.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_menu_audiobook.png b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_audiobook.png new file mode 100644 index 0000000..b81f0d8 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_audiobook.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_menu_downloaded.png b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_downloaded.png new file mode 100644 index 0000000..360b190 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_downloaded.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_menu_fav.png b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_fav.png new file mode 100644 index 0000000..99bc85c Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_fav.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_menu_library.png b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_library.png new file mode 100644 index 0000000..7c436f2 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_library.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_menu_new.png b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_new.png new file mode 100644 index 0000000..b8a3ec9 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_new.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_menu_search.png b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_search.png new file mode 100644 index 0000000..a0f26ea Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_search.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_menu_star.png b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_star.png new file mode 100644 index 0000000..a107a90 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_menu_star.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_news.png b/Android/app/src/main/res/drawable-xxhdpi/ic_news.png new file mode 100644 index 0000000..457821b Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_news.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_next.png b/Android/app/src/main/res/drawable-xxhdpi/ic_next.png new file mode 100644 index 0000000..047cac1 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_next.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/Android/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100755 index 0000000..6806174 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_notification_player.png b/Android/app/src/main/res/drawable-xxhdpi/ic_notification_player.png new file mode 100755 index 0000000..f439c2f Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_notification_player.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/Android/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png new file mode 100755 index 0000000..1e71ba3 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png b/Android/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png new file mode 100755 index 0000000..4abfe88 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_prev.png b/Android/app/src/main/res/drawable-xxhdpi/ic_prev.png new file mode 100644 index 0000000..c13fd86 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_prev.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_reader_dark.png b/Android/app/src/main/res/drawable-xxhdpi/ic_reader_dark.png new file mode 100644 index 0000000..8da397f Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_reader_dark.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_reader_light.png b/Android/app/src/main/res/drawable-xxhdpi/ic_reader_light.png new file mode 100644 index 0000000..6b53468 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_reader_light.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_reload.png b/Android/app/src/main/res/drawable-xxhdpi/ic_reload.png new file mode 100644 index 0000000..0a0b0aa Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_reload.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_search.png b/Android/app/src/main/res/drawable-xxhdpi/ic_search.png new file mode 100644 index 0000000..a55cf8d Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_search.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_search_new.png b/Android/app/src/main/res/drawable-xxhdpi/ic_search_new.png new file mode 100644 index 0000000..63f28d0 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_search_new.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_settings.png b/Android/app/src/main/res/drawable-xxhdpi/ic_settings.png new file mode 100644 index 0000000..1b76825 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_settings.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_share.png b/Android/app/src/main/res/drawable-xxhdpi/ic_share.png new file mode 100644 index 0000000..6133031 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_share.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png b/Android/app/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png new file mode 100755 index 0000000..616e47f Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png b/Android/app/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png new file mode 100755 index 0000000..1ec9d65 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_speaker_big.png b/Android/app/src/main/res/drawable-xxhdpi/ic_speaker_big.png new file mode 100644 index 0000000..3c57099 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_speaker_big.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_speaker_mid.png b/Android/app/src/main/res/drawable-xxhdpi/ic_speaker_mid.png new file mode 100644 index 0000000..34a2570 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_speaker_mid.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_speaker_small.png b/Android/app/src/main/res/drawable-xxhdpi/ic_speaker_small.png new file mode 100644 index 0000000..562afee Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_speaker_small.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_star_big_active.png b/Android/app/src/main/res/drawable-xxhdpi/ic_star_big_active.png new file mode 100644 index 0000000..4729a76 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_star_big_active.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_star_mid.png b/Android/app/src/main/res/drawable-xxhdpi/ic_star_mid.png new file mode 100644 index 0000000..db3c0d1 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_star_mid.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_star_small_selected.png b/Android/app/src/main/res/drawable-xxhdpi/ic_star_small_selected.png new file mode 100644 index 0000000..31f173c Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_star_small_selected.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_stat_image_audiotrack.png b/Android/app/src/main/res/drawable-xxhdpi/ic_stat_image_audiotrack.png new file mode 100755 index 0000000..f214042 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_stat_image_audiotrack.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_toggle.png b/Android/app/src/main/res/drawable-xxhdpi/ic_toggle.png new file mode 100644 index 0000000..0bf9ad1 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_toggle.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/ic_trash.png b/Android/app/src/main/res/drawable-xxhdpi/ic_trash.png new file mode 100644 index 0000000..52990ef Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/ic_trash.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/logo_fnp.png b/Android/app/src/main/res/drawable-xxhdpi/logo_fnp.png new file mode 100644 index 0000000..fff2d88 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/logo_fnp.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/logo_mkidn.png b/Android/app/src/main/res/drawable-xxhdpi/logo_mkidn.png new file mode 100644 index 0000000..321a9d3 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/logo_mkidn.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/logo_opp.png b/Android/app/src/main/res/drawable-xxhdpi/logo_opp.png new file mode 100644 index 0000000..ab9dc7d Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/logo_opp.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/logo_wl_light.png b/Android/app/src/main/res/drawable-xxhdpi/logo_wl_light.png new file mode 100644 index 0000000..625e113 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/logo_wl_light.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/player_chapter_next.png b/Android/app/src/main/res/drawable-xxhdpi/player_chapter_next.png new file mode 100644 index 0000000..ac87045 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/player_chapter_next.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/player_chapter_previous.png b/Android/app/src/main/res/drawable-xxhdpi/player_chapter_previous.png new file mode 100644 index 0000000..b7f547f Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/player_chapter_previous.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/player_controls_forward.png b/Android/app/src/main/res/drawable-xxhdpi/player_controls_forward.png new file mode 100644 index 0000000..d5b5660 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/player_controls_forward.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/player_controls_pause.png b/Android/app/src/main/res/drawable-xxhdpi/player_controls_pause.png new file mode 100644 index 0000000..d6ee4ff Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/player_controls_pause.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/player_controls_play.png b/Android/app/src/main/res/drawable-xxhdpi/player_controls_play.png new file mode 100644 index 0000000..5b1d376 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/player_controls_play.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/player_controls_rewind.png b/Android/app/src/main/res/drawable-xxhdpi/player_controls_rewind.png new file mode 100644 index 0000000..da33a0f Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/player_controls_rewind.png differ diff --git a/Android/app/src/main/res/drawable-xxhdpi/splash.png b/Android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000..9dfa7b0 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/font_big.png b/Android/app/src/main/res/drawable-xxxhdpi/font_big.png new file mode 100644 index 0000000..14bc8cc Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/font_big.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/font_small.png b/Android/app/src/main/res/drawable-xxxhdpi/font_small.png new file mode 100644 index 0000000..9f26bb3 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/font_small.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_about.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_about.png new file mode 100644 index 0000000..87854c2 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_about.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_accept.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_accept.png new file mode 100644 index 0000000..0b66b3a Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_accept.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_accept_new.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_accept_new.png new file mode 100644 index 0000000..2403499 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_accept_new.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_book.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_book.png new file mode 100644 index 0000000..550287a Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_book.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_close.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_close.png new file mode 100644 index 0000000..5d82a06 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_close.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_close_small.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_close_small.png new file mode 100644 index 0000000..a836d7e Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_close_small.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_comment.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_comment.png new file mode 100644 index 0000000..f7e3259 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_comment.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_fav.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_fav.png new file mode 100644 index 0000000..039bc53 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_fav.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_fav_active.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_fav_active.png new file mode 100644 index 0000000..1d0eac4 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_fav_active.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_fav_big.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_fav_big.png new file mode 100644 index 0000000..c77fa2e Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_fav_big.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_fav_big_active.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_fav_big_active.png new file mode 100644 index 0000000..326049e Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_fav_big_active.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_filter.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_filter.png new file mode 100644 index 0000000..17cb52f Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_filter.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_filter_new.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_filter_new.png new file mode 100644 index 0000000..fab4b3e Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_filter_new.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_glass_big.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_glass_big.png new file mode 100644 index 0000000..5db3741 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_glass_big.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_glass_mid.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_glass_mid.png new file mode 100644 index 0000000..434fa5c Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_glass_mid.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_glass_small.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_glass_small.png new file mode 100644 index 0000000..1aa4187 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_glass_small.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_all.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_all.png new file mode 100644 index 0000000..116ef33 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_all.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_audiobook.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_audiobook.png new file mode 100644 index 0000000..7b583c2 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_audiobook.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_downloaded.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_downloaded.png new file mode 100644 index 0000000..7b5fdea Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_downloaded.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_fav.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_fav.png new file mode 100644 index 0000000..b53d493 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_fav.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_library.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_library.png new file mode 100644 index 0000000..a202537 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_library.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_new.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_new.png new file mode 100644 index 0000000..edf74d2 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_new.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_search.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_search.png new file mode 100644 index 0000000..b077c81 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_search.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_star.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_star.png new file mode 100644 index 0000000..eabd4d7 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_menu_star.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_news.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_news.png new file mode 100644 index 0000000..650073e Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_news.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_next.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_next.png new file mode 100644 index 0000000..188fa25 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_next.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_notification.png new file mode 100755 index 0000000..4b4be84 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_notification_player.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_notification_player.png new file mode 100755 index 0000000..338a097 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_notification_player.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_prev.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_prev.png new file mode 100644 index 0000000..5d5f61a Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_prev.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_reader_dark.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_reader_dark.png new file mode 100644 index 0000000..c9533d5 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_reader_dark.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_reader_light.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_reader_light.png new file mode 100644 index 0000000..309491c Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_reader_light.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_reload.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_reload.png new file mode 100644 index 0000000..04d3629 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_reload.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_search.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_search.png new file mode 100644 index 0000000..78f95df Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_search.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_search_new.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_search_new.png new file mode 100644 index 0000000..682f152 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_search_new.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_settings.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_settings.png new file mode 100644 index 0000000..2936608 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_settings.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_share.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_share.png new file mode 100644 index 0000000..190b43e Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_share.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_big.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_big.png new file mode 100644 index 0000000..88ad1d8 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_big.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_mid.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_mid.png new file mode 100644 index 0000000..3854990 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_mid.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_small.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_small.png new file mode 100644 index 0000000..6adc66c Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_small.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_star_big_active.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_star_big_active.png new file mode 100644 index 0000000..b2ec6dc Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_star_big_active.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_star_mid.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_star_mid.png new file mode 100644 index 0000000..5c2a3c9 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_star_mid.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_star_small_selected.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_star_small_selected.png new file mode 100644 index 0000000..878923a Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_star_small_selected.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_toggle.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_toggle.png new file mode 100644 index 0000000..7cd32c7 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_toggle.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/ic_trash.png b/Android/app/src/main/res/drawable-xxxhdpi/ic_trash.png new file mode 100644 index 0000000..9278f30 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/ic_trash.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/logo_fnp.png b/Android/app/src/main/res/drawable-xxxhdpi/logo_fnp.png new file mode 100644 index 0000000..66db78e Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/logo_fnp.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/logo_mkidn.png b/Android/app/src/main/res/drawable-xxxhdpi/logo_mkidn.png new file mode 100644 index 0000000..87eba3c Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/logo_mkidn.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/logo_opp.png b/Android/app/src/main/res/drawable-xxxhdpi/logo_opp.png new file mode 100644 index 0000000..81b7342 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/logo_opp.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/logo_wl_light.png b/Android/app/src/main/res/drawable-xxxhdpi/logo_wl_light.png new file mode 100644 index 0000000..80e5fc0 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/logo_wl_light.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/player_chapter_next.png b/Android/app/src/main/res/drawable-xxxhdpi/player_chapter_next.png new file mode 100644 index 0000000..8ac4426 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/player_chapter_next.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/player_chapter_previous.png b/Android/app/src/main/res/drawable-xxxhdpi/player_chapter_previous.png new file mode 100644 index 0000000..cdb1eb1 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/player_chapter_previous.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/player_controls_forward.png b/Android/app/src/main/res/drawable-xxxhdpi/player_controls_forward.png new file mode 100644 index 0000000..86723ab Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/player_controls_forward.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/player_controls_pause.png b/Android/app/src/main/res/drawable-xxxhdpi/player_controls_pause.png new file mode 100644 index 0000000..e853410 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/player_controls_pause.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/player_controls_play.png b/Android/app/src/main/res/drawable-xxxhdpi/player_controls_play.png new file mode 100644 index 0000000..ff0fa13 Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/player_controls_play.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/player_controls_rewind.png b/Android/app/src/main/res/drawable-xxxhdpi/player_controls_rewind.png new file mode 100644 index 0000000..ec0c77c Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/player_controls_rewind.png differ diff --git a/Android/app/src/main/res/drawable-xxxhdpi/splash.png b/Android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 0000000..ad331fc Binary files /dev/null and b/Android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/Android/app/src/main/res/drawable/accept_orange_tint.xml b/Android/app/src/main/res/drawable/accept_orange_tint.xml new file mode 100644 index 0000000..b6105f6 --- /dev/null +++ b/Android/app/src/main/res/drawable/accept_orange_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/accept_tint.xml b/Android/app/src/main/res/drawable/accept_tint.xml new file mode 100644 index 0000000..fc29851 --- /dev/null +++ b/Android/app/src/main/res/drawable/accept_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/background_geen.xml b/Android/app/src/main/res/drawable/background_geen.xml new file mode 100644 index 0000000..3a1e257 --- /dev/null +++ b/Android/app/src/main/res/drawable/background_geen.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/close_filters_selector.xml b/Android/app/src/main/res/drawable/close_filters_selector.xml new file mode 100644 index 0000000..b4da8f9 --- /dev/null +++ b/Android/app/src/main/res/drawable/close_filters_selector.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/close_small_dark_tint.xml b/Android/app/src/main/res/drawable/close_small_dark_tint.xml new file mode 100644 index 0000000..acd345b --- /dev/null +++ b/Android/app/src/main/res/drawable/close_small_dark_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/close_small_tint.xml b/Android/app/src/main/res/drawable/close_small_tint.xml new file mode 100644 index 0000000..dfe1965 --- /dev/null +++ b/Android/app/src/main/res/drawable/close_small_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/delete_icon_selector.xml b/Android/app/src/main/res/drawable/delete_icon_selector.xml new file mode 100644 index 0000000..5225fdd --- /dev/null +++ b/Android/app/src/main/res/drawable/delete_icon_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/filter_background.xml b/Android/app/src/main/res/drawable/filter_background.xml new file mode 100644 index 0000000..15d3a6c --- /dev/null +++ b/Android/app/src/main/res/drawable/filter_background.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/app/src/main/res/drawable/filter_tint.xml b/Android/app/src/main/res/drawable/filter_tint.xml new file mode 100644 index 0000000..e02e3a0 --- /dev/null +++ b/Android/app/src/main/res/drawable/filter_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml b/Android/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml new file mode 100644 index 0000000..38fbc26 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/Android/app/src/main/res/drawable/ic_arrow_forward_white_24dp.xml b/Android/app/src/main/res/drawable/ic_arrow_forward_white_24dp.xml new file mode 100644 index 0000000..3760095 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_arrow_forward_white_24dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/Android/app/src/main/res/drawable/ic_arrow_right_24dp.xml b/Android/app/src/main/res/drawable/ic_arrow_right_24dp.xml new file mode 100644 index 0000000..32c9f8f --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_arrow_right_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/Android/app/src/main/res/drawable/ic_arrow_right_white_24dp.xml b/Android/app/src/main/res/drawable/ic_arrow_right_white_24dp.xml new file mode 100644 index 0000000..6ddda40 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_arrow_right_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/Android/app/src/main/res/drawable/ic_fav_tint_orange_light.xml b/Android/app/src/main/res/drawable/ic_fav_tint_orange_light.xml new file mode 100644 index 0000000..5884e55 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_fav_tint_orange_light.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/ic_glass_mid_tint_white.xml b/Android/app/src/main/res/drawable/ic_glass_mid_tint_white.xml new file mode 100644 index 0000000..807d592 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_glass_mid_tint_white.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/ic_launcher_background.xml b/Android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..5713f34 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/app/src/main/res/drawable/ic_menu_star_tint_white.xml b/Android/app/src/main/res/drawable/ic_menu_star_tint_white.xml new file mode 100644 index 0000000..30ef280 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_menu_star_tint_white.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/ic_next_tint.xml b/Android/app/src/main/res/drawable/ic_next_tint.xml new file mode 100644 index 0000000..70e3f7f --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_next_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/ic_prev_gray_tint.xml b/Android/app/src/main/res/drawable/ic_prev_gray_tint.xml new file mode 100644 index 0000000..b5696d3 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_prev_gray_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/ic_prev_tint.xml b/Android/app/src/main/res/drawable/ic_prev_tint.xml new file mode 100644 index 0000000..b430b65 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_prev_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/ic_refresh_white_24dp.xml b/Android/app/src/main/res/drawable/ic_refresh_white_24dp.xml new file mode 100644 index 0000000..a8175c3 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_refresh_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/Android/app/src/main/res/drawable/ic_speaker_mid_tint_white.xml b/Android/app/src/main/res/drawable/ic_speaker_mid_tint_white.xml new file mode 100644 index 0000000..3c977e4 --- /dev/null +++ b/Android/app/src/main/res/drawable/ic_speaker_mid_tint_white.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/ic_todo.png b/Android/app/src/main/res/drawable/ic_todo.png new file mode 100644 index 0000000..862b0bc Binary files /dev/null and b/Android/app/src/main/res/drawable/ic_todo.png differ diff --git a/Android/app/src/main/res/drawable/nav_item_background.xml b/Android/app/src/main/res/drawable/nav_item_background.xml new file mode 100644 index 0000000..42a64cf --- /dev/null +++ b/Android/app/src/main/res/drawable/nav_item_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/nav_item_background_selector.xml b/Android/app/src/main/res/drawable/nav_item_background_selector.xml new file mode 100644 index 0000000..e2ecafd --- /dev/null +++ b/Android/app/src/main/res/drawable/nav_item_background_selector.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/next_con_tint.xml b/Android/app/src/main/res/drawable/next_con_tint.xml new file mode 100644 index 0000000..fc027a4 --- /dev/null +++ b/Android/app/src/main/res/drawable/next_con_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/next_icon_selector.xml b/Android/app/src/main/res/drawable/next_icon_selector.xml new file mode 100644 index 0000000..1860bfa --- /dev/null +++ b/Android/app/src/main/res/drawable/next_icon_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/next_selector.xml b/Android/app/src/main/res/drawable/next_selector.xml new file mode 100644 index 0000000..ac5747c --- /dev/null +++ b/Android/app/src/main/res/drawable/next_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/orange_details_round_rect.xml b/Android/app/src/main/res/drawable/orange_details_round_rect.xml new file mode 100644 index 0000000..bdbdb6f --- /dev/null +++ b/Android/app/src/main/res/drawable/orange_details_round_rect.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/orange_round_rect.xml b/Android/app/src/main/res/drawable/orange_round_rect.xml new file mode 100644 index 0000000..55e2d7d --- /dev/null +++ b/Android/app/src/main/res/drawable/orange_round_rect.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/pause_selector.xml b/Android/app/src/main/res/drawable/pause_selector.xml new file mode 100644 index 0000000..e7f79f3 --- /dev/null +++ b/Android/app/src/main/res/drawable/pause_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/pause_tint.xml b/Android/app/src/main/res/drawable/pause_tint.xml new file mode 100644 index 0000000..f0eaba1 --- /dev/null +++ b/Android/app/src/main/res/drawable/pause_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/play_selector.xml b/Android/app/src/main/res/drawable/play_selector.xml new file mode 100644 index 0000000..c6aa8b9 --- /dev/null +++ b/Android/app/src/main/res/drawable/play_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/play_tint.xml b/Android/app/src/main/res/drawable/play_tint.xml new file mode 100644 index 0000000..ac3535e --- /dev/null +++ b/Android/app/src/main/res/drawable/play_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/play_white_tint.xml b/Android/app/src/main/res/drawable/play_white_tint.xml new file mode 100644 index 0000000..ec9a15b --- /dev/null +++ b/Android/app/src/main/res/drawable/play_white_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/prev_con_selector.xml b/Android/app/src/main/res/drawable/prev_con_selector.xml new file mode 100644 index 0000000..a68d60a --- /dev/null +++ b/Android/app/src/main/res/drawable/prev_con_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/prev_con_tint.xml b/Android/app/src/main/res/drawable/prev_con_tint.xml new file mode 100644 index 0000000..d0d0ce0 --- /dev/null +++ b/Android/app/src/main/res/drawable/prev_con_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/prev_selector.xml b/Android/app/src/main/res/drawable/prev_selector.xml new file mode 100644 index 0000000..d56f0a4 --- /dev/null +++ b/Android/app/src/main/res/drawable/prev_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/progress_background.xml b/Android/app/src/main/res/drawable/progress_background.xml new file mode 100644 index 0000000..8b2ab2e --- /dev/null +++ b/Android/app/src/main/res/drawable/progress_background.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/refresh_active_tint.xml b/Android/app/src/main/res/drawable/refresh_active_tint.xml new file mode 100644 index 0000000..c06cda8 --- /dev/null +++ b/Android/app/src/main/res/drawable/refresh_active_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/refresh_icon_light_selector.xml b/Android/app/src/main/res/drawable/refresh_icon_light_selector.xml new file mode 100644 index 0000000..f4a550d --- /dev/null +++ b/Android/app/src/main/res/drawable/refresh_icon_light_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/refresh_icon_selector.xml b/Android/app/src/main/res/drawable/refresh_icon_selector.xml new file mode 100644 index 0000000..f7eae49 --- /dev/null +++ b/Android/app/src/main/res/drawable/refresh_icon_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/refresh_tint.xml b/Android/app/src/main/res/drawable/refresh_tint.xml new file mode 100644 index 0000000..d64d53e --- /dev/null +++ b/Android/app/src/main/res/drawable/refresh_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/refresh_tint_white.xml b/Android/app/src/main/res/drawable/refresh_tint_white.xml new file mode 100644 index 0000000..2c8a621 --- /dev/null +++ b/Android/app/src/main/res/drawable/refresh_tint_white.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/round_background_overlay.xml b/Android/app/src/main/res/drawable/round_background_overlay.xml new file mode 100644 index 0000000..ef0b13d --- /dev/null +++ b/Android/app/src/main/res/drawable/round_background_overlay.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/search_tint.xml b/Android/app/src/main/res/drawable/search_tint.xml new file mode 100644 index 0000000..7257e88 --- /dev/null +++ b/Android/app/src/main/res/drawable/search_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/seekbar_progress.xml b/Android/app/src/main/res/drawable/seekbar_progress.xml new file mode 100644 index 0000000..6c93427 --- /dev/null +++ b/Android/app/src/main/res/drawable/seekbar_progress.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/seekbar_thumb_color.xml b/Android/app/src/main/res/drawable/seekbar_thumb_color.xml new file mode 100644 index 0000000..5ff81b8 --- /dev/null +++ b/Android/app/src/main/res/drawable/seekbar_thumb_color.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/selector_button_white_border.xml b/Android/app/src/main/res/drawable/selector_button_white_border.xml new file mode 100644 index 0000000..57d641f --- /dev/null +++ b/Android/app/src/main/res/drawable/selector_button_white_border.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/toggle_selector.xml b/Android/app/src/main/res/drawable/toggle_selector.xml new file mode 100644 index 0000000..c2b3dfe --- /dev/null +++ b/Android/app/src/main/res/drawable/toggle_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/toggle_tint.xml b/Android/app/src/main/res/drawable/toggle_tint.xml new file mode 100644 index 0000000..f21e14f --- /dev/null +++ b/Android/app/src/main/res/drawable/toggle_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/trash_active_tint.xml b/Android/app/src/main/res/drawable/trash_active_tint.xml new file mode 100644 index 0000000..ad1499d --- /dev/null +++ b/Android/app/src/main/res/drawable/trash_active_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/trash_tint.xml b/Android/app/src/main/res/drawable/trash_tint.xml new file mode 100644 index 0000000..ff47951 --- /dev/null +++ b/Android/app/src/main/res/drawable/trash_tint.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/drawable/white_star.xml b/Android/app/src/main/res/drawable/white_star.xml new file mode 100644 index 0000000..30ef280 --- /dev/null +++ b/Android/app/src/main/res/drawable/white_star.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/Android/app/src/main/res/layout-v21/activity_login.xml b/Android/app/src/main/res/layout-v21/activity_login.xml new file mode 100644 index 0000000..2eb057a --- /dev/null +++ b/Android/app/src/main/res/layout-v21/activity_login.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + +