diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 8598a75..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Yapplication \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 97626ba..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 2ab1e27..7990923 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -3,22 +3,16 @@ diff --git a/app/.gitignore b/app/.gitignore index 796b96d..09fb41b 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,2 @@ /build +.idea/* \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index e4ec7db..cca059d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,10 +2,21 @@ apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'me.tatarka.retrolambda' +def keystorePropertiesFile = rootProject.file("keystore.properties") +def keystoreProperties = new Properties() +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + android { +// signingConfigs { +// Yappl { +// keyAlias keystoreProperties['keyAlias'] +// keyPassword keystoreProperties['keyPassword'] +// storeFile file(keystoreProperties['storeFile']) +// storePassword keystoreProperties['storePassword'] +// } +// } compileSdkVersion 23 - buildToolsVersion "23.0.2" - + buildToolsVersion "23.0.3" defaultConfig { applicationId "ru.aleien.yapplication" minSdkVersion 15 @@ -15,7 +26,7 @@ android { } buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } @@ -24,12 +35,6 @@ android { } } - sourceSets { - androidTest { - setRoot('src/test') - } - } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -37,35 +42,56 @@ android { } dependencies { - ext.supportVersion = '23.3.0' - compile fileTree(dir: 'libs', include: ['*.jar']) - compile "com.android.support:support-v4:$supportVersion" - compile "com.android.support:appcompat-v7:$supportVersion" - compile "com.android.support:design:$supportVersion" + ext.supportVersion = '24.1.1' + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.android.support:support-v4:24.1.1' + compile 'com.android.support:appcompat-v7:24.1.1' + compile 'com.android.support:design:24.1.1' // Annotation heaven - compile 'com.jakewharton:butterknife:7.0.1' + compile 'com.jakewharton:butterknife:8.2.1' + apt 'com.jakewharton:butterknife-compiler:8.2.1' compile 'javax.annotation:jsr250-api:1.0' + compile 'com.hannesdorfmann.fragmentargs:annotation:3.0.2' + apt 'com.hannesdorfmann.fragmentargs:processor:3.0.2' + + compile 'frankiesardo:icepick:3.2.0' + provided 'frankiesardo:icepick-processor:3.2.0' + // UI - compile "com.android.support:cardview-v7:$supportVersion" - compile "com.android.support:recyclerview-v7:$supportVersion" + compile 'com.android.support:cardview-v7:24.1.1' + compile 'com.android.support:recyclerview-v7:24.1.1' // Testing testCompile 'junit:junit:4.12' - androidTestCompile "com.android.support:support-annotations:$supportVersion" - androidTestCompile 'com.android.support.test:runner:0.4.1' - testCompile "org.robolectric:robolectric:3.0" - testCompile "org.mockito:mockito-core:1.10.19" + androidTestCompile "com.android.support:support-annotations:24.1.1" + androidTestCompile 'com.android.support.test:runner:0.5' + testCompile 'org.mockito:mockito-core:2.0.99-beta' + testCompile "org.robolectric:robolectric:3.1.2" + testCompile 'org.robolectric:shadows-support-v4:3.1.2' // Network compile 'com.squareup.okhttp:okhttp:2.7.5' - compile 'com.squareup.okhttp3:logging-interceptor:3.0.1' - compile 'com.squareup.retrofit2:converter-gson:2.0.0' - compile 'com.squareup.retrofit2:retrofit:2.0.0' - + compile 'com.squareup.okhttp3:logging-interceptor:3.4.1' + compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' + compile 'com.squareup.retrofit2:converter-gson:2.1.0' + compile 'com.squareup.retrofit2:retrofit:2.1.0' debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2' // Image processor compile 'com.github.bumptech.glide:glide:3.7.0' + + // DI + compile 'com.google.dagger:dagger:2.6' + apt 'com.google.dagger:dagger-compiler:2.6' + provided 'javax.annotation:jsr250-api:1.0' + + // Rx + compile 'io.reactivex:rxandroid:1.2.1' + compile 'io.reactivex:rxjava:1.1.8' + + debugCompile 'com.facebook.stetho:stetho:1.2.0' + compile 'com.jakewharton.timber:timber:4.1.2' + } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0a0c3b5..49e0649 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -15,3 +15,26 @@ #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} + +# Butterknife +-keep public class * implements butterknife.internal.ViewBinder { public (); } + +# Prevent obfuscation of types which use ButterKnife annotations since the simple name +# is used to reflectively look up the generated ViewBinder. +-keep class butterknife.* +-keepclasseswithmembernames class * { @butterknife.* ; } +-keepclasseswithmembernames class * { @butterknife.* ; } +-keepnames class * { @butterknife.Bind *;} + +# Dagger +-keepclassmembers,allowobfuscation class * { + @javax.inject.* *; + @dagger.* *; + (); +} + +-keep class javax.inject.** { *; } +-keep class **$$ModuleAdapter +-keep class **$$InjectAdapter +-keep class **$$StaticInjection +-keep class dagger.** { *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cd8d13b..0a40382 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,14 +4,17 @@ + - + diff --git a/app/src/main/java/ru/aleien/yapplication/App.java b/app/src/main/java/ru/aleien/yapplication/App.java new file mode 100644 index 0000000..c91dcac --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/App.java @@ -0,0 +1,30 @@ +package ru.aleien.yapplication; + +import android.app.Application; + +import com.facebook.stetho.Stetho; + +import ru.aleien.yapplication.di.AppComponent; +import ru.aleien.yapplication.di.AppModule; +import ru.aleien.yapplication.di.DaggerAppComponent; +import timber.log.Timber; + +public class App extends Application { + private AppComponent component; + + + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree()); + Stetho.initializeWithDefaults(this.getApplicationContext()); + component = DaggerAppComponent.builder() + .appModule(new AppModule(this)) + .build(); + + } + + public AppComponent dagger() { + return component; + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/ArtistClickHandler.java b/app/src/main/java/ru/aleien/yapplication/ArtistClickHandler.java index 12cd5ee..a04b464 100644 --- a/app/src/main/java/ru/aleien/yapplication/ArtistClickHandler.java +++ b/app/src/main/java/ru/aleien/yapplication/ArtistClickHandler.java @@ -1,7 +1,9 @@ package ru.aleien.yapplication; +import java.io.Serializable; + import ru.aleien.yapplication.model.Artist; -public interface ArtistClickHandler { +public interface ArtistClickHandler extends Serializable { void artistClicked(Artist artist); } diff --git a/app/src/main/java/ru/aleien/yapplication/ArtistsPresenter.java b/app/src/main/java/ru/aleien/yapplication/ArtistsPresenter.java index 0496e90..39dd66e 100644 --- a/app/src/main/java/ru/aleien/yapplication/ArtistsPresenter.java +++ b/app/src/main/java/ru/aleien/yapplication/ArtistsPresenter.java @@ -1,14 +1,17 @@ package ru.aleien.yapplication; -import android.content.Context; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; +import android.util.Log; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.List; +import javax.inject.Inject; + import ru.aleien.yapplication.base.BasePresenter; +import ru.aleien.yapplication.database.DBBackend; import ru.aleien.yapplication.dataprovider.ArtistsProvider; import ru.aleien.yapplication.dataprovider.WebArtistsProvider; import ru.aleien.yapplication.model.Artist; @@ -16,7 +19,13 @@ import ru.aleien.yapplication.screens.detailedinfo.ArtistInfoView; import ru.aleien.yapplication.screens.list.ArtistsListView; import ru.aleien.yapplication.screens.list.ArtistsRecyclerFragment; +import ru.aleien.yapplication.screens.list.ArtistsView; +import ru.aleien.yapplication.screens.tabs.ArtistsTabsFragment; import ru.aleien.yapplication.utils.adapters.ArtistsRecyclerAdapter; +import rx.Completable; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; +import timber.log.Timber; /** * Created by aleien on 09.04.16. @@ -25,17 +34,33 @@ */ public class ArtistsPresenter extends BasePresenter implements ArtistsRequester, ArtistClickHandler, Serializable { ArtistsProvider artistsProvider; - private WeakReference> artistsListView; + private final DBBackend dbSource; + private WeakReference artistsListView; private WeakReference currentFragment; - public ArtistsPresenter(Context context) { - artistsProvider = new WebArtistsProvider(this, context); + @Inject + public ArtistsPresenter(DBBackend dbSource, + // Как здесь получать интерфейс? + WebArtistsProvider artistsProvider) { + this.dbSource = dbSource; + this.artistsProvider = artistsProvider; } @Override - public void takeListView(ArtistsListView list) { + public void takeListView(ArtistsView list) { artistsListView = new WeakReference<>(list); - artistsProvider.requestData(); + + subscribe(dbSource.getAllArtists() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::provideData, + throwable -> Timber.e("DBError", "Error while reading cached artists"))); + + artistsProvider.requestData() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::provideData, + e -> Timber.e(e, "takeListView -> requestData")); } @Override @@ -46,7 +71,16 @@ public void takeDetailedView(ArtistInfoView info, Artist artist) { @Override public void provideData(List response) { - artistsListView.get().setAdapter(new ArtistsRecyclerAdapter(response, this)); + if (artistsListView != null && artistsListView.get() != null) { + artistsListView.get().showContent(response, this); + } + dbSource.clearArtists(); + Completable.fromAction(() -> { + Timber.e("Working on: " + Thread.currentThread().getName()); + for (Artist artist : response) { + dbSource.insertArtist(artist); + } + }).subscribeOn(Schedulers.io()).subscribe(); } @Override @@ -60,11 +94,14 @@ public void artistClicked(Artist artist) { @Override public void onStart() { if (currentFragment == null) { - ArtistsRecyclerFragment artistsListFragment = new ArtistsRecyclerFragment(); + ArtistsTabsFragment artistsListFragment = new ArtistsTabsFragment(); takeListView(artistsListFragment); currentFragment = new WeakReference<>(artistsListFragment); + // Из-за этого места не происходит нормально восстановить фрагмент! + // Нужно как-то сообщать презентеру, что происходит ПЕРЕсоздание + // Тут просто архитектурная ошибка, не знаю, как сходу поправить + getView().changeFragmentTo(currentFragment.get(), currentFragment.get() instanceof ArtistInfoFragment); } - getView().changeFragmentTo(currentFragment.get(), currentFragment.get() instanceof ArtistInfoFragment); } } diff --git a/app/src/main/java/ru/aleien/yapplication/ArtistsRequester.java b/app/src/main/java/ru/aleien/yapplication/ArtistsRequester.java index 4b152ca..b263aa5 100644 --- a/app/src/main/java/ru/aleien/yapplication/ArtistsRequester.java +++ b/app/src/main/java/ru/aleien/yapplication/ArtistsRequester.java @@ -7,6 +7,7 @@ import ru.aleien.yapplication.model.Artist; import ru.aleien.yapplication.screens.detailedinfo.ArtistInfoView; import ru.aleien.yapplication.screens.list.ArtistsListView; +import ru.aleien.yapplication.screens.list.ArtistsView; /** * Created by aleien on 09.04.16. @@ -17,6 +18,6 @@ public interface ArtistsRequester { void takeDetailedView(ArtistInfoView infoView, Artist artist); - void takeListView(ArtistsListView listView); + void takeListView(ArtistsView listView); void provideData(List response); } diff --git a/app/src/main/java/ru/aleien/yapplication/ListArtistsActivity.java b/app/src/main/java/ru/aleien/yapplication/ListArtistsActivity.java deleted file mode 100644 index 4c72636..0000000 --- a/app/src/main/java/ru/aleien/yapplication/ListArtistsActivity.java +++ /dev/null @@ -1,83 +0,0 @@ -package ru.aleien.yapplication; - -import android.os.Bundle; -import android.os.PersistableBundle; -import android.support.v4.app.Fragment; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.view.MenuItem; - -public class ListArtistsActivity extends AppCompatActivity implements MainView { - private ArtistsPresenter artistsPresenter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - setupToolbar(); - instantiatePresenter(savedInstanceState); - - } - - private void instantiatePresenter(Bundle savedInstanceState) { - if (savedInstanceState != null && savedInstanceState.containsKey("presenterState")) { - artistsPresenter = (ArtistsPresenter) savedInstanceState.get("presenterState"); - } else { - artistsPresenter = new ArtistsPresenter(this); - } - } - - @Override - protected void onStart() { - super.onStart(); - artistsPresenter.attachView(this); - artistsPresenter.onStart(); - } - - @Override - protected void onStop() { - super.onStop(); - artistsPresenter.detachView(); - } - - @Override - public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { - super.onSaveInstanceState(outState, outPersistentState); - outState.putSerializable("presenterState", artistsPresenter); - } - - @SuppressWarnings("ConstantConditions") - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - if (menuItem.getItemId() == android.R.id.home) { - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - getSupportFragmentManager().popBackStack(); - } - return super.onOptionsItemSelected(menuItem); - } - - @SuppressWarnings("ConstantConditions") - private void setupToolbar() { - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - getSupportActionBar().setDisplayShowHomeEnabled(true); - } - - @SuppressWarnings("ConstantConditions") - @Override - public void changeFragmentTo(Fragment fragment, boolean hideBackButton) { - getSupportActionBar().setDisplayHomeAsUpEnabled(hideBackButton); - getSupportFragmentManager().beginTransaction() - .replace(R.id.fragment_container, fragment) - .addToBackStack(null) - .commit(); - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - //noinspection ConstantConditions - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - } -} diff --git a/app/src/main/java/ru/aleien/yapplication/MainActivity.java b/app/src/main/java/ru/aleien/yapplication/MainActivity.java new file mode 100644 index 0000000..be4ddb9 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/MainActivity.java @@ -0,0 +1,189 @@ +package ru.aleien.yapplication; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import javax.inject.Inject; + +import butterknife.BindString; +import butterknife.ButterKnife; +import ru.aleien.yapplication.screens.list.AboutDialogFragment; +import ru.aleien.yapplication.utils.PendingIntentBuilder; + +public class MainActivity extends AppCompatActivity implements MainView { + private final static int MUSIC_ID = 1010; + private final static int RADIO_ID = 1020; + + private BroadcastReceiver broadcastReceiver; + @Inject ArtistsPresenter artistsPresenter; + + @BindString(R.string.about_title) String aboutTitle; + @BindString(R.string.about_message) String aboutMessage; + @BindString(R.string.title_dismiss) String dismissTitle; + @BindString(R.string.email_author) String emailTo; + @BindString(R.string.email_title) String emailTitle; + private final static Intent EMAIL_INTENT = new Intent(Intent.ACTION_SENDTO) + .setType("text/plain") + .setData(Uri.parse("mailto:")) + .putExtra(Intent.EXTRA_EMAIL, new String[]{"technogenom@gmail.com"}) + .putExtra(Intent.EXTRA_SUBJECT, "Re: Yapplication"); + + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ((App) getApplication()).dagger().inject(this); + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + showHeadphonesNotification(audioManager.isWiredHeadsetOn() + || audioManager.isBluetoothA2dpOn() + || audioManager.isBluetoothScoOn()); + + } + }; + + setContentView(R.layout.activity_main); + ButterKnife.bind(this); + setupToolbar(); + } + + @Override + protected void onStart() { + super.onStart(); + artistsPresenter.attachView(this); + artistsPresenter.onStart(); + + registerReceiver(broadcastReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + + } + + @Override + protected void onStop() { + super.onStop(); + artistsPresenter.detachView(); + unregisterReceiver(broadcastReceiver); + } + + @Override + public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { + super.onSaveInstanceState(outState, outPersistentState); + outState.putSerializable("presenterState", artistsPresenter); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main_menu, menu); + return true; + } + + @SuppressWarnings("ConstantConditions") + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case android.R.id.home: + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + getSupportFragmentManager().popBackStack(); + break; + case R.id.menu_about: + showAbout(); + break; + case R.id.menu_contact: + composeEmail(); + break; + } + + return super.onOptionsItemSelected(menuItem); + } + + @SuppressWarnings("ConstantConditions") + private void setupToolbar() { + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + @SuppressWarnings("ConstantConditions") + @Override + public void changeFragmentTo(Fragment fragment, boolean hideBackButton) { + getSupportActionBar().setDisplayHomeAsUpEnabled(hideBackButton); + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragment_container, fragment) + .addToBackStack(null) + .commit(); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + //noinspection ConstantConditions + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + } + + // TODO: Вынести в отдельный класс + // TODO: При открытой странице инфо об артисте, открывать страницу артиста + private void showHeadphonesNotification(boolean wiredHeadsetOn) { + + PendingIntent musicPendingIntent = PendingIntentBuilder.buildOpenMarketPendingIntent(MUSIC_ID, "ru.yandex.music", this); + PendingIntent radioPendingIntent = PendingIntentBuilder.buildOpenMarketPendingIntent(RADIO_ID, "ru.yandex.radio", this); + + int musicNotificationId = 001; + + if (wiredHeadsetOn) { + NotificationCompat.Builder musicNotificationBuilder = + new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.ic_stat_hardware_headset) + .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) + .setContentTitle("Headphones plugged in") + .addAction(R.drawable.ic_stat_yamusic, "Ya.Music", musicPendingIntent) + .addAction(R.drawable.ic_stat_hardware_headset, "Ya.Radio", radioPendingIntent) + .setContentText("Open in:"); + + NotificationManager mNotifyMgr = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + mNotifyMgr.notify(musicNotificationId, musicNotificationBuilder.build()); + } else { + NotificationManager mNotifyMgr = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + mNotifyMgr.cancel(musicNotificationId); + } + + } + + private void composeEmail() { + if (EMAIL_INTENT.resolveActivity(getPackageManager()) != null) { + startActivity(EMAIL_INTENT); + } + + } + + private void showAbout() { + DialogFragment aboutFragment = AboutDialogFragment + .newInstance(aboutTitle, aboutMessage); + aboutFragment.show(getSupportFragmentManager(), "dialog"); + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/base/BaseFragment.java b/app/src/main/java/ru/aleien/yapplication/base/BaseFragment.java new file mode 100644 index 0000000..43595f9 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/base/BaseFragment.java @@ -0,0 +1,34 @@ +package ru.aleien.yapplication.base; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import butterknife.ButterKnife; +import butterknife.Unbinder; + + +public class BaseFragment extends Fragment { + Unbinder unbinder; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + unbinder = ButterKnife.bind(this, view); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/base/BasePresenter.java b/app/src/main/java/ru/aleien/yapplication/base/BasePresenter.java index 39c9ab4..e7c685c 100644 --- a/app/src/main/java/ru/aleien/yapplication/base/BasePresenter.java +++ b/app/src/main/java/ru/aleien/yapplication/base/BasePresenter.java @@ -2,6 +2,9 @@ import java.lang.ref.WeakReference; +import rx.Subscription; +import rx.subscriptions.CompositeSubscription; + /** * Created by aleien on 21.04.16. * Базовый класс для презентера, отвечает за сохранение ссылки на представление (вьюху). @@ -9,14 +12,20 @@ */ public abstract class BasePresenter { private WeakReference view; + private CompositeSubscription subs = new CompositeSubscription(); public abstract void onStart(); + public void subscribe(Subscription sub) { + subs.add(sub); + } + public void attachView(V view) { this.view = new WeakReference<>(view); } public void detachView() { + subs.clear(); view.clear(); view = null; } diff --git a/app/src/main/java/ru/aleien/yapplication/database/DBBackend.java b/app/src/main/java/ru/aleien/yapplication/database/DBBackend.java new file mode 100644 index 0000000..eda51f4 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/database/DBBackend.java @@ -0,0 +1,134 @@ +package ru.aleien.yapplication.database; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import ru.aleien.yapplication.model.Artist; +import rx.Observable; + +import static ru.aleien.yapplication.database.DBContract.GenreToArtist.ARTIST_ID; +import static ru.aleien.yapplication.database.DBContract.GenreToArtist.GENRE_ID; +import static ru.aleien.yapplication.database.DBContract.GenreToArtist.TABLE; +import static ru.aleien.yapplication.database.DBContract.allColumns; + +public class DBBackend { + + private DBHelper dbOpenHelper; + + @Inject + public DBBackend(DBHelper helper) { + dbOpenHelper = helper; + } + + public void insertArtist(Artist artist) { + insertArtist(artist.name, + artist.tracks, + artist.albums, + artist.link, + artist.description, + artist.cover.small, + artist.cover.big, + artist.genres); + } + + // Напрашивается билдер + void insertArtist(String name, + int tracks, + int albums, + String link, + String description, + String small_cover, + String big_cover, + List genres) { + SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); + db.beginTransaction(); + + try { + ContentValues contentValues = new ContentValues(); + contentValues.put(DBContract.Artists.NAME, name); + contentValues.put(DBContract.Artists.TRACKS, tracks); + contentValues.put(DBContract.Artists.ALBUMS, albums); + contentValues.put(DBContract.Artists.LINK, link); + contentValues.put(DBContract.Artists.DESCRIPTION, description); + contentValues.put(DBContract.Artists.SMALL_COVER, small_cover); + contentValues.put(DBContract.Artists.BIG_COVER, big_cover); + + long artistId = db.insert(DBContract.Artists.TABLE, null, contentValues); + List genresIds = insertGenres(genres); + + insertRelation(artistId, genresIds); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + + } + + void insertRelation(long artistId, List genresIds) { + SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); + for (Long genreId : genresIds) { + ContentValues contentValues = new ContentValues(); + contentValues.put(ARTIST_ID, artistId); + contentValues.put(GENRE_ID, genreId); + db.insert(TABLE, null, contentValues); + } + } + + List insertGenres(List genres) { + SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); + List rowIds = new ArrayList<>(); + ContentValues contentValues = new ContentValues(); + for (String genre : genres) { + contentValues.put(DBContract.Genres.NAME, genre); + long rowId = db.insertWithOnConflict(DBContract.Genres.TABLE, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE); + rowIds.add(rowId); + } + + return rowIds; + + } + + public Observable> getAllArtists() { + return Observable.fromCallable(this::loadArtists); + } + + List loadArtists() { + List artists = new ArrayList<>(); + SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(DBContract.Artists.TABLE, + allColumns, null, null, null, null, null); + cursor.moveToFirst(); + + while (!cursor.isAfterLast()) { + artists.add(cursorToArtist(cursor)); + cursor.moveToNext(); + } + + cursor.close(); + return artists; + } + + public void clearArtists() { + dbOpenHelper.getWritableDatabase().execSQL("DELETE FROM " + DBContract.Artists.TABLE); + } + + private Artist cursorToArtist(Cursor cursor) { + return new Artist(cursor.getInt(cursor.getColumnIndex(DBContract.Artists.ID)), + cursor.getString(cursor.getColumnIndex(DBContract.Artists.NAME)), + new ArrayList<>(), + cursor.getInt(cursor.getColumnIndex(DBContract.Artists.TRACKS)), + cursor.getInt(cursor.getColumnIndex(DBContract.Artists.ALBUMS)), + cursor.getString(cursor.getColumnIndex(DBContract.Artists.LINK)), + cursor.getString(cursor.getColumnIndex(DBContract.Artists.DESCRIPTION)), + new Artist.Cover(cursor.getString(cursor.getColumnIndex(DBContract.Artists.SMALL_COVER)), + cursor.getString(cursor.getColumnIndex(DBContract.Artists.BIG_COVER)))); + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/database/DBContract.java b/app/src/main/java/ru/aleien/yapplication/database/DBContract.java new file mode 100644 index 0000000..abf6523 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/database/DBContract.java @@ -0,0 +1,45 @@ +package ru.aleien.yapplication.database; + +/** + * Created by aleien on 08.08.16. + */ + +public interface DBContract { + interface Artists { + String TABLE = "artists"; + String ID = "id"; + String NAME = "name"; + String TRACKS = "tracks"; + String ALBUMS = "albums"; + String LINK = "link"; + String DESCRIPTION = "description"; + String SMALL_COVER = "small_cover"; + String BIG_COVER = "big_cover"; + } + + interface Genres { + String TABLE = "genres"; + String NAME = "genre"; + + } + + interface GenreToArtist { + String TABLE = "genre_to_artist"; + String ARTIST_ID = "artist_id"; + String GENRE_ID = "genre_id"; + + } + + String DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS "; + + String[] allColumns = new String[]{ + Artists.ID, + Artists.NAME, + Artists.TRACKS, + Artists.ALBUMS, + Artists.LINK, + Artists.DESCRIPTION, + Artists.SMALL_COVER, + Artists.BIG_COVER + }; +} diff --git a/app/src/main/java/ru/aleien/yapplication/database/DBHelper.java b/app/src/main/java/ru/aleien/yapplication/database/DBHelper.java new file mode 100644 index 0000000..9b68837 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/database/DBHelper.java @@ -0,0 +1,64 @@ +package ru.aleien.yapplication.database; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import ru.aleien.yapplication.model.Artist; + +import static ru.aleien.yapplication.database.DBContract.DROP_TABLE_IF_EXISTS; + +/** + * Created by user on 22.07.16. + */ + +@Singleton +public class DBHelper extends SQLiteOpenHelper { + + @Inject + public DBHelper(Context context, @Named("dbName") String name) { + super(context, name, null, 1); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL( + "CREATE TABLE " + DBContract.Artists.TABLE + + "(" + + DBContract.Artists.NAME + " TEXT," + + DBContract.Artists.TRACKS + " INTEGER," + + DBContract.Artists.ALBUMS + " INTEGER," + + DBContract.Artists.LINK + " TEXT," + + DBContract.Artists.DESCRIPTION + " TEXT," + + DBContract.Artists.SMALL_COVER + " TEXT," + + DBContract.Artists.BIG_COVER + " TEXT" + + ")" + ); + + db.execSQL( + "CREATE TABLE " + + DBContract.Genres.TABLE + + " (" + + DBContract.Genres.NAME + " STRING UNIQUE" + + ")"); + + db.execSQL( + "CREATE TABLE " + DBContract.GenreToArtist.TABLE + + " (" + + DBContract.GenreToArtist.ARTIST_ID + " INTEGER," + + DBContract.GenreToArtist.GENRE_ID + " INTEGER )"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int i, int i1) { + db.execSQL(DROP_TABLE_IF_EXISTS + DBContract.Artists.TABLE); + db.execSQL(DROP_TABLE_IF_EXISTS + DBContract.Genres.TABLE); + db.execSQL(DROP_TABLE_IF_EXISTS + DBContract.GenreToArtist.TABLE); + onCreate(db); + } + +} diff --git a/app/src/main/java/ru/aleien/yapplication/dataprovider/ArtistsProvider.java b/app/src/main/java/ru/aleien/yapplication/dataprovider/ArtistsProvider.java index 901e925..a9854fc 100644 --- a/app/src/main/java/ru/aleien/yapplication/dataprovider/ArtistsProvider.java +++ b/app/src/main/java/ru/aleien/yapplication/dataprovider/ArtistsProvider.java @@ -1,9 +1,15 @@ package ru.aleien.yapplication.dataprovider; +import java.util.List; + +import ru.aleien.yapplication.model.Artist; +import rx.Observable; + /** * Created by aleien on 09.04.16. * Интерфейс между контроллером и поставщиком данных */ public interface ArtistsProvider { - void requestData(); + // Лучше дженерик? + Observable> requestData(); } diff --git a/app/src/main/java/ru/aleien/yapplication/dataprovider/WebArtistsProvider.java b/app/src/main/java/ru/aleien/yapplication/dataprovider/WebArtistsProvider.java index 962b84f..720db16 100644 --- a/app/src/main/java/ru/aleien/yapplication/dataprovider/WebArtistsProvider.java +++ b/app/src/main/java/ru/aleien/yapplication/dataprovider/WebArtistsProvider.java @@ -1,23 +1,12 @@ package ru.aleien.yapplication.dataprovider; -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import java.io.IOException; import java.util.List; -import okhttp3.Cache; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import ru.aleien.yapplication.ArtistsRequester; +import javax.inject.Inject; + +import ru.aleien.yapplication.di.AppModule; import ru.aleien.yapplication.model.Artist; -import ru.aleien.yapplication.utils.Utils; +import rx.Observable; /** * Created by aleien on 09.04.16. @@ -25,40 +14,17 @@ */ public class WebArtistsProvider implements ArtistsProvider { - private static final String JSON_URL = "http://cache-default03g.cdn.yandex.net/download.cdn.yandex.net/mobilization-2016/artists.json"; - private final ArtistsRequester artistsRequester; - private OkHttpClient client; - public WebArtistsProvider(ArtistsRequester artistsRequester, Context context) { - this.artistsRequester = artistsRequester; - Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = Utils.createInterceptor(context); - Cache cache = Utils.getCache(context); - client = new OkHttpClient.Builder() - .cache(cache) - .addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR) - .build(); + private AppModule.Api api; + + @Inject + public WebArtistsProvider(AppModule.Api api) { + this.api = api; } @Override - public void requestData() { - Handler mainHandler = new Handler(Looper.getMainLooper()); - Request request = new Request.Builder() - .url(JSON_URL) - .build(); - - client.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - Log.d("Main", "FAIL"); - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - List responseList = Utils.decodeResponse(response); - mainHandler.post(() -> artistsRequester.provideData(responseList)); // Очень странно, что onResponse выполняется не в main-треде - // Вместо хэндлера можно, например, использовать rx-яву, которая очень хорошо дружит с ретрофитом - } - }); + public Observable> requestData() { + return api.getArtists(); } } diff --git a/app/src/main/java/ru/aleien/yapplication/di/AppComponent.java b/app/src/main/java/ru/aleien/yapplication/di/AppComponent.java new file mode 100644 index 0000000..6a7ab21 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/di/AppComponent.java @@ -0,0 +1,14 @@ +package ru.aleien.yapplication.di; + +import javax.inject.Singleton; + +import dagger.Component; +import ru.aleien.yapplication.MainActivity; + +@Singleton +@Component(modules = AppModule.class) +public interface AppComponent { + + void inject(MainActivity mainActivity); + +} diff --git a/app/src/main/java/ru/aleien/yapplication/di/AppModule.java b/app/src/main/java/ru/aleien/yapplication/di/AppModule.java new file mode 100644 index 0000000..97cc5cc --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/di/AppModule.java @@ -0,0 +1,86 @@ +package ru.aleien.yapplication.di; + +import android.app.Application; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; + +import java.util.List; + +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.http.GET; +import ru.aleien.yapplication.model.Artist; +import rx.Observable; + +import static android.content.Context.MODE_PRIVATE; + +/** + * Created by user on 23.07.16. + */ +@Module +public class AppModule { + private static final String BASE_URL = "http://cache-default03g.cdn.yandex.net/download.cdn.yandex.net/mobilization-2016/"; + + private Context context; + private String dbName = "ArtistsDB"; + + public AppModule(Application application) { + this.context = application; + } + + @Provides + @Singleton + Context provideContext() { + return this.context; + } + + @Provides + @Singleton + SQLiteDatabase provideDatabase(Context context) { + return context.openOrCreateDatabase(dbName, MODE_PRIVATE, null); + } + + @Provides + @Named("dbName") + String provideDBName() { + return dbName; + } + + @Provides + @Singleton + Retrofit provideRestClient(OkHttpClient client) { + return new Retrofit.Builder() + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .baseUrl(BASE_URL) + .client(client) + .build(); + } + + @Provides + @Singleton + OkHttpClient provideOkhttpClient() { + return new OkHttpClient.Builder() + .addInterceptor(new HttpLoggingInterceptor()) + .build(); + } + + @Provides + Api provideApi(Retrofit rest) { + return rest.create(Api.class); + } + + public interface Api { + @GET("artists.json") + Observable> getArtists(); + } + +} diff --git a/app/src/main/java/ru/aleien/yapplication/model/Artist.java b/app/src/main/java/ru/aleien/yapplication/model/Artist.java index b9d39de..d8e0254 100644 --- a/app/src/main/java/ru/aleien/yapplication/model/Artist.java +++ b/app/src/main/java/ru/aleien/yapplication/model/Artist.java @@ -1,8 +1,12 @@ package ru.aleien.yapplication.model; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.Serializable; import java.util.List; -public class Artist { +public class Artist implements Parcelable { public final int id; public final String name; public final List genres; @@ -23,7 +27,7 @@ public Artist(int id, String name, List genres, int tracks, int albums, this.cover = cover; } - public static class Cover { + public static class Cover implements Parcelable { public final String small; public final String big; @@ -31,6 +35,73 @@ public Cover(String small, String big) { this.small = small; this.big = big; } + + protected Cover(Parcel in) { + small = in.readString(); + big = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(small); + dest.writeString(big); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public Cover createFromParcel(Parcel in) { + return new Cover(in); + } + + @Override + public Cover[] newArray(int size) { + return new Cover[size]; + } + }; } + protected Artist(Parcel in) { + id = in.readInt(); + name = in.readString(); + genres = in.createStringArrayList(); + tracks = in.readInt(); + albums = in.readInt(); + link = in.readString(); + description = in.readString(); + cover = in.readParcelable(Cover.class.getClassLoader()); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(id); + dest.writeString(name); + dest.writeStringList(genres); + dest.writeInt(tracks); + dest.writeInt(albums); + dest.writeString(link); + dest.writeString(description); + dest.writeParcelable(cover, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public Artist createFromParcel(Parcel in) { + return new Artist(in); + } + + @Override + public Artist[] newArray(int size) { + return new Artist[size]; + } + }; } diff --git a/app/src/main/java/ru/aleien/yapplication/screens/detailedinfo/ArtistInfoFragment.java b/app/src/main/java/ru/aleien/yapplication/screens/detailedinfo/ArtistInfoFragment.java index 90019a1..c13c4a4 100644 --- a/app/src/main/java/ru/aleien/yapplication/screens/detailedinfo/ArtistInfoFragment.java +++ b/app/src/main/java/ru/aleien/yapplication/screens/detailedinfo/ArtistInfoFragment.java @@ -2,6 +2,7 @@ import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; @@ -10,9 +11,13 @@ import android.widget.ImageView; import android.widget.TextView; +import com.hannesdorfmann.fragmentargs.FragmentArgs; +import com.hannesdorfmann.fragmentargs.annotation.Arg; +import com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs; + import java.util.Locale; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; import ru.aleien.yapplication.R; import ru.aleien.yapplication.model.Artist; @@ -24,17 +29,26 @@ * Created by aleien on 09.04.16. * Фрагмент для отображения информации о музыканте. */ +@FragmentWithArgs public class ArtistInfoFragment extends Fragment implements ArtistInfoView { - @Bind(R.id.info_cover) + @BindView(R.id.info_cover) ImageView cover; - @Bind(R.id.info_genres) + @BindView(R.id.info_genres) TextView genres; - @Bind(R.id.info_music) + @BindView(R.id.info_music) TextView infoMusic; - @Bind(R.id.info_bio) + @BindView(R.id.info_bio) TextView bio; - private Artist artist; + @Arg + @NonNull + Artist artist; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FragmentArgs.inject(this); + } @Nullable @Override @@ -50,14 +64,12 @@ public void onStart() { setup(); } - // TODO: форматирование строки в зависимости от количества песен/альбомов private void setup() { - if (artist != null) { - ImageLoader.getInstance().loadImage(getContext(), cover, Uri.parse(artist.cover.big)); - genres.setText(convertToString(artist.genres, ',')); - infoMusic.setText(String.format(Locale.getDefault(), getResources().getString(R.string.music_info), artist.albums, artist.tracks)); - bio.setText(artist.description); - } + ImageLoader.getInstance().loadImage(getContext(), cover, Uri.parse(artist.cover.big)); + genres.setText(convertToString(artist.genres, ',')); + infoMusic.setText(String.format(Locale.getDefault(), getResources().getString(R.string.music_info), artist.albums, artist.tracks)); + bio.setText(artist.description); + } @Override diff --git a/app/src/main/java/ru/aleien/yapplication/screens/list/AboutDialogFragment.java b/app/src/main/java/ru/aleien/yapplication/screens/list/AboutDialogFragment.java new file mode 100644 index 0000000..270ad35 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/screens/list/AboutDialogFragment.java @@ -0,0 +1,34 @@ +package ru.aleien.yapplication.screens.list; + +import android.app.Dialog; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; + +import ru.aleien.yapplication.R; + +public class AboutDialogFragment extends DialogFragment { + + public static AboutDialogFragment newInstance(String title, String message) { + AboutDialogFragment frag = new AboutDialogFragment(); + Bundle args = new Bundle(); + args.putString("title", title); + args.putString("message", message); + frag.setArguments(args); + return frag; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + String title = getArguments().getString("title"); + String message = getArguments().getString("message"); + + return new AlertDialog.Builder(getActivity()) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.alert_dialog_ok, + (dialog, whichButton) -> dismiss() + ) + .create(); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsRecyclerFragment.java b/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsRecyclerFragment.java index d7a3a77..85da5bc 100644 --- a/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsRecyclerFragment.java +++ b/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsRecyclerFragment.java @@ -9,46 +9,53 @@ import android.view.View; import android.view.ViewGroup; -import butterknife.Bind; +import java.util.List; + +import butterknife.BindView; import butterknife.ButterKnife; +import butterknife.Unbinder; +import ru.aleien.yapplication.ArtistClickHandler; import ru.aleien.yapplication.R; +import ru.aleien.yapplication.model.Artist; import ru.aleien.yapplication.utils.adapters.ArtistsRecyclerAdapter; /** * Created by aleien on 24.04.16. * Фрагмент для отображения списка музыкантов. */ -public class ArtistsRecyclerFragment extends Fragment implements ArtistsListView { - @Bind(R.id.artists_list) +public class ArtistsRecyclerFragment extends Fragment implements ArtistsView { + @BindView(R.id.artists_list) RecyclerView artistsRecycler; private ArtistsRecyclerAdapter adapter; + private Unbinder unbinder; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View fragmentView = inflater.inflate(R.layout.fragment_artists_recycler, container, false); - ButterKnife.bind(this, fragmentView); + unbinder = ButterKnife.bind(this, fragmentView); return fragmentView; } + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - artistsRecycler.setLayoutManager(new LinearLayoutManager(view.getContext(), LinearLayoutManager.VERTICAL, false)); + artistsRecycler.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); artistsRecycler.setAdapter(adapter); artistsRecycler.setHasFixedSize(true); } - @Override - public void setAdapter(RecyclerView.Adapter adapter) { - if (adapter instanceof ArtistsRecyclerAdapter) { - this.adapter = (ArtistsRecyclerAdapter) adapter; - - if (this.isAdded()) { - artistsRecycler.setAdapter(adapter); - } - } + @Override + public void showContent(List artists, ArtistClickHandler handler) { + adapter = new ArtistsRecyclerAdapter(artists, handler); + if (isAdded()) artistsRecycler.setAdapter(adapter); } } diff --git a/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsView.java b/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsView.java new file mode 100644 index 0000000..291d9cf --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsView.java @@ -0,0 +1,14 @@ +package ru.aleien.yapplication.screens.list; + +import java.util.List; + +import ru.aleien.yapplication.ArtistClickHandler; +import ru.aleien.yapplication.model.Artist; + +/** + * Created by aleien on 09.08.16. + */ + +public interface ArtistsView { + void showContent(List artists, ArtistClickHandler handler); +} diff --git a/app/src/main/java/ru/aleien/yapplication/screens/tabs/ArtistTabFragment.java b/app/src/main/java/ru/aleien/yapplication/screens/tabs/ArtistTabFragment.java new file mode 100644 index 0000000..bdc6518 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/screens/tabs/ArtistTabFragment.java @@ -0,0 +1,65 @@ +package ru.aleien.yapplication.screens.tabs; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.hannesdorfmann.fragmentargs.FragmentArgs; +import com.hannesdorfmann.fragmentargs.annotation.Arg; +import com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs; + +import butterknife.BindBool; +import butterknife.BindView; +import butterknife.OnClick; +import ru.aleien.yapplication.R; +import ru.aleien.yapplication.base.BaseFragment; +import ru.aleien.yapplication.model.Artist; +import ru.aleien.yapplication.utils.ImageLoader; + +@FragmentWithArgs +public class ArtistTabFragment extends BaseFragment { + @BindView(R.id.tab_image) + ImageView artistImage; + @BindBool(R.bool.is_tablet) boolean isTablet; + + @Arg + @NonNull // Студия подчеркивает, что not initialized. Как тут быть? + Artist artist; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FragmentArgs.inject(this); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_tab, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ImageLoader.getInstance().loadImageCropped(getContext(), artistImage, artist.cover.big); + } + + @OnClick(R.id.tab_more_button) + public void onMoreButtonClicked() { + InfoDialogFragment infoFragment = new InfoDialogFragmentBuilder(artist).build(); + if (!isTablet) { + getFragmentManager() + .beginTransaction() + .replace(R.id.fragment_container, infoFragment) + .addToBackStack(null) + .commit(); + + } else { + infoFragment.show(getFragmentManager(), "dialog"); + } + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/screens/tabs/ArtistsTabsAdapter.java b/app/src/main/java/ru/aleien/yapplication/screens/tabs/ArtistsTabsAdapter.java new file mode 100644 index 0000000..73e3448 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/screens/tabs/ArtistsTabsAdapter.java @@ -0,0 +1,39 @@ +package ru.aleien.yapplication.screens.tabs; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; + +import java.util.ArrayList; +import java.util.List; + +import ru.aleien.yapplication.ArtistClickHandler; +import ru.aleien.yapplication.model.Artist; + +class ArtistsTabsAdapter extends FragmentStatePagerAdapter { + private List artists = new ArrayList(); + + public ArtistsTabsAdapter(FragmentManager fm, List artists, ArtistClickHandler handler) { + super(fm); + this.artists.addAll(artists); + } + + public void setArtists(List artists) { + this.artists.addAll(artists); + } + + @Override + public Fragment getItem(int position) { + return new ArtistTabFragmentBuilder(artists.get(position)).build(); + } + + @Override + public int getCount() { + return artists.size(); + } + + @Override + public CharSequence getPageTitle(int position) { + return artists.get(position).name; + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/screens/tabs/ArtistsTabsFragment.java b/app/src/main/java/ru/aleien/yapplication/screens/tabs/ArtistsTabsFragment.java new file mode 100644 index 0000000..436e7c6 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/screens/tabs/ArtistsTabsFragment.java @@ -0,0 +1,138 @@ +package ru.aleien.yapplication.screens.tabs; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.view.ViewPager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.OnPageChange; +import butterknife.Optional; +import icepick.Icepick; +import icepick.State; +import ru.aleien.yapplication.ArtistClickHandler; +import ru.aleien.yapplication.R; +import ru.aleien.yapplication.base.BaseFragment; +import ru.aleien.yapplication.model.Artist; +import ru.aleien.yapplication.screens.list.ArtistsView; +import ru.aleien.yapplication.utils.adapters.ArtistsRecyclerAdapter; +import timber.log.Timber; + +public class ArtistsTabsFragment extends BaseFragment implements ArtistsView, ArtistClickHandler { + @Nullable + @BindView(R.id.tabs_viewpager) + ViewPager viewPager; + @Nullable + @BindView(R.id.tabs) + TabLayout tabs; + + @Nullable + @BindView(R.id.tabs_recycler) + RecyclerView recycler; + @Nullable ArtistsRecyclerAdapter listAdapter; + @Nullable + @BindView(R.id.tabs_container) + FrameLayout container; + + List cached = new ArrayList<>(); + @State Artist currentArtist; + @State int currentPage; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_artists_tabs, container, false); + } + + // Чтобы это заработало, надо поправить архитектурную ошибку - презентер в onStart руками пересоздает фрагмент + // Это неправильно. + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Icepick.restoreInstanceState(this, savedInstanceState); + if (cached.size() != 0) + showContent(new ArrayList<>(cached), null); + + if (recycler != null) { + recycler.setLayoutManager(new LinearLayoutManager(getContext())); + if (listAdapter == null) + listAdapter = new ArtistsRecyclerAdapter(cached, this); + recycler.setAdapter(listAdapter); + + if (currentArtist != null) + setFragment(new ArtistTabFragmentBuilder(currentArtist).build()); + } + + if (viewPager != null && tabs != null) { + // тут нужно происзвести какую-то магию, потому что сейчас те страницы, которые были в памяти до + // открытия детализированной информации, не пересоздаются и не переоткрыватся + // Почему? + viewPager.setAdapter(new ArtistsTabsAdapter(getFragmentManager(), cached, this)); + tabs.setupWithViewPager(viewPager); + tabs.setVisibility(View.VISIBLE); + viewPager.setCurrentItem(currentPage, false); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + Icepick.saveInstanceState(this, outState); + } + + @Override + public void showContent(List artists, ArtistClickHandler handler) { + cached.clear(); + cached.addAll(artists); + + if (currentArtist == null) currentArtist = cached.get(0); + if (viewPager != null && tabs != null) { + viewPager.setAdapter(new ArtistsTabsAdapter(getFragmentManager(), artists, handler)); + tabs.setupWithViewPager(viewPager); + tabs.setVisibility(View.VISIBLE); + viewPager.setCurrentItem(currentPage, false); + } + + // Проверки в коде на наличие вьюх? Получается один фрагмент отвечает и за планшетную, и за + // портретную ориентацию. Лучше сделать отдельный фрагмент для того и другого, мне кажется + if (recycler != null) { + // Это тоже как-то неправильно. Мне кажется, адаптер должен в одном месте задаваться + if (listAdapter == null) listAdapter = new ArtistsRecyclerAdapter(cached, this); + recycler.setAdapter(listAdapter); + + setFragment(new ArtistTabFragmentBuilder(currentArtist).build()); + } + } + + @Optional + @OnPageChange(value = R.id.tabs_viewpager, + callback = OnPageChange.Callback.PAGE_SELECTED) + public void onPageChanged(int page) { + Timber.d("Selected page: %d", page); + currentPage = page; + } + + @Override + public void artistClicked(Artist artist) { + currentArtist = artist; + setFragment(new ArtistTabFragmentBuilder(artist).build()); + } + + private void setFragment(Fragment fragment) { + if (container != null) { + getFragmentManager().beginTransaction() + .replace(R.id.tabs_container, fragment) + .commit(); + } + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/screens/tabs/InfoDialogFragment.java b/app/src/main/java/ru/aleien/yapplication/screens/tabs/InfoDialogFragment.java new file mode 100644 index 0000000..f904f73 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/screens/tabs/InfoDialogFragment.java @@ -0,0 +1,120 @@ +package ru.aleien.yapplication.screens.tabs; + +import android.app.Dialog; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.hannesdorfmann.fragmentargs.FragmentArgs; +import com.hannesdorfmann.fragmentargs.annotation.Arg; +import com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs; + +import java.util.Locale; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; +import ru.aleien.yapplication.R; +import ru.aleien.yapplication.model.Artist; +import ru.aleien.yapplication.utils.ImageLoader; + +import static ru.aleien.yapplication.utils.Utils.convertToString; + +@FragmentWithArgs +public class InfoDialogFragment extends DialogFragment { + @Arg + @NonNull // Студия подчеркивает, что not initialized. Как бороться? + Artist artist; + Unbinder unbinder; + + @BindView(R.id.info_cover) + ImageView cover; + @BindView(R.id.info_genres) + TextView genres; + @BindView(R.id.info_music) + TextView infoMusic; + @BindView(R.id.info_bio) + TextView bio; + + + int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; + int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + int padding = (int) (Resources.getSystem().getDisplayMetrics().density * 32); + + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FragmentArgs.inject(this); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View fragment = inflater.inflate(R.layout.fragment_artist_page, container, false); + unbinder = ButterKnife.bind(this, fragment); + + return fragment; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setup(); + + } + + public void onResume() { + super.onResume(); + Dialog dialog = getDialog(); + if (dialog != null) { + Window window = getDialog().getWindow(); + assert window != null; + window.setLayout(screenWidth - 2 * padding, screenHeight - 2 * padding); + window.setGravity(Gravity.CENTER); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + private void setup() { + ImageLoader.getInstance().loadImageCropped(getContext(), cover, artist.cover.big); + genres.setText(convertToString(artist.genres, ',')); + // Надо заюзать plurals + infoMusic.setText(String.format(Locale.getDefault(), getResources().getString(R.string.music_info), artist.albums, artist.tracks)); + bio.setText(artist.description); + + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + View fragment = LayoutInflater.from(getContext()).inflate(R.layout.fragment_artist_page, null); // T___T Как здесь по-нормальному сделать-то? + unbinder = ButterKnife.bind(this, fragment); + setup(); + ((LinearLayout.LayoutParams) cover.getLayoutParams()).height = screenHeight / 2; + + return new AlertDialog.Builder(getActivity()) + .setTitle(artist.name) + .setView(fragment) + .setPositiveButton(R.string.alert_dialog_ok, + (dialog, whichButton) -> dismiss() + ) + .create(); + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/utils/ImageLoader.java b/app/src/main/java/ru/aleien/yapplication/utils/ImageLoader.java index 339727e..8293670 100644 --- a/app/src/main/java/ru/aleien/yapplication/utils/ImageLoader.java +++ b/app/src/main/java/ru/aleien/yapplication/utils/ImageLoader.java @@ -1,10 +1,15 @@ package ru.aleien.yapplication.utils; import android.content.Context; +import android.graphics.Bitmap; import android.net.Uri; import android.widget.ImageView; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.Transformation; +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; +import com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapper; import ru.aleien.yapplication.R; @@ -32,6 +37,10 @@ public static ImageLoader getInstance() { return instance; } + public void loadImageCropped(Context context, ImageView imageView, String url) { + loadImageCropped(context, imageView, Uri.parse(url)); + } + public void loadImageCropped(Context context, ImageView imageView, Uri uri) { Glide.with(context) .load(uri) @@ -40,6 +49,11 @@ public void loadImageCropped(Context context, ImageView imageView, Uri uri) { .into(imageView); } + + public void loadImage(Context context, ImageView imageView, String url) { + loadImage(context, imageView, Uri.parse(url)); + } + public void loadImage(Context context, ImageView imageView, Uri uri) { Glide.with(context) .load(uri) diff --git a/app/src/main/java/ru/aleien/yapplication/utils/PendingIntentBuilder.java b/app/src/main/java/ru/aleien/yapplication/utils/PendingIntentBuilder.java new file mode 100644 index 0000000..04075e1 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/utils/PendingIntentBuilder.java @@ -0,0 +1,42 @@ +package ru.aleien.yapplication.utils; + +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; + +/** + * Created by user on 19.07.16. + */ +public class PendingIntentBuilder { + private static final String playStore = "market://details?id="; + + public static PendingIntent buildOpenMarketPendingIntent(int id, String pack, Context context) { + Intent intent = buildOpenAppOrMarketPageIntent(pack, context); + return PendingIntent.getActivity(context, id, intent, 0); + } + + public static Intent buildOpenAppOrMarketPageIntent(String pack, Context context) { + + try { + if (Utils.checkPackageExists(context, pack)) { + PackageManager pm = context.getPackageManager(); + return pm.getLaunchIntentForPackage(pack); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(playStore + pack)); + return intent; + } + } catch (ActivityNotFoundException e) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(playStore + pack)); + return intent; + } + + } + + + +} diff --git a/app/src/main/java/ru/aleien/yapplication/utils/Utils.java b/app/src/main/java/ru/aleien/yapplication/utils/Utils.java index 6b5c718..b46b6ce 100644 --- a/app/src/main/java/ru/aleien/yapplication/utils/Utils.java +++ b/app/src/main/java/ru/aleien/yapplication/utils/Utils.java @@ -1,22 +1,11 @@ package ru.aleien.yapplication.utils; import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.support.annotation.NonNull; +import android.content.pm.PackageManager; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Type; import java.util.List; -import okhttp3.Cache; -import okhttp3.Interceptor; -import okhttp3.Response; -import ru.aleien.yapplication.model.Artist; +import timber.log.Timber; public class Utils { @@ -33,49 +22,25 @@ public static String convertToString(List list, Character separator) { return resultString.toString(); } - public static boolean isNetworkAvailable(Context context) { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getApplicationContext() - .getSystemService(Context.CONNECTIVITY_SERVICE); - - NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); - return (activeNetworkInfo != null) && (activeNetworkInfo.isConnected()); - } - - @NonNull - public static Interceptor createInterceptor(Context context) { - return chain -> { - Response originalResponse = chain.proceed(chain.request()); - if (Utils.isNetworkAvailable(context)) { - int maxAge = 60; - return originalResponse.newBuilder() - .header("Cache-Control", "public, max-age=" + maxAge) - .build(); - } else { - int maxStale = 60 * 60 * 24 * 28; - return originalResponse.newBuilder() - .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale) - .build(); - } - }; - } + public static String getAppVersion(Context context) { + String versionCode = "1.0"; + try { + versionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; + } catch (PackageManager.NameNotFoundException e) { + Timber.e(e, "getAppVersion: Could not get package info"); + e.printStackTrace(); + } - public static Cache getCache(Context context) { - File httpCacheDirectory = new File(context.getCacheDir(), "responses"); - int cacheSize = 10 * 1024 * 1024; // 10 MiB - return new Cache(httpCacheDirectory, cacheSize); + return versionCode; } - public static List decodeResponse(Response response) { - List resultList = null; - Type listType = new TypeToken>() { - }.getType(); + public static boolean checkPackageExists(Context context, String targetPackage) { try { - resultList = new Gson().fromJson(response.body().string(), listType); - } catch (IOException e) { - e.printStackTrace(); + context.getPackageManager().getPackageInfo(targetPackage, PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + return false; } - - return resultList; + return true; } diff --git a/app/src/main/java/ru/aleien/yapplication/utils/adapters/ArtistsRecyclerAdapter.java b/app/src/main/java/ru/aleien/yapplication/utils/adapters/ArtistsRecyclerAdapter.java index e291a57..9e38077 100644 --- a/app/src/main/java/ru/aleien/yapplication/utils/adapters/ArtistsRecyclerAdapter.java +++ b/app/src/main/java/ru/aleien/yapplication/utils/adapters/ArtistsRecyclerAdapter.java @@ -1,6 +1,8 @@ package ru.aleien.yapplication.utils.adapters; import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; @@ -9,14 +11,16 @@ import android.widget.RelativeLayout; import android.widget.TextView; +import java.lang.ref.WeakReference; import java.util.List; import java.util.Locale; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; import ru.aleien.yapplication.ArtistClickHandler; import ru.aleien.yapplication.R; import ru.aleien.yapplication.model.Artist; +import ru.aleien.yapplication.screens.tabs.ArtistsTabsFragment; import ru.aleien.yapplication.utils.ImageLoader; import ru.aleien.yapplication.utils.Utils; @@ -26,12 +30,12 @@ */ public class ArtistsRecyclerAdapter extends RecyclerView.Adapter { - private final List artists; - private final ArtistClickHandler clickHandler; + @NonNull private final List artists; + @Nullable private WeakReference clickHandler; - public ArtistsRecyclerAdapter(List artists, ArtistClickHandler clickHandler) { + public ArtistsRecyclerAdapter(@NonNull List artists, ArtistClickHandler clickHandler) { this.artists = artists; - this.clickHandler = clickHandler; + this.clickHandler = new WeakReference<>(clickHandler); } @Override @@ -50,7 +54,13 @@ public void onBindViewHolder(ArtistHolder holder, int position) { holder.musicInfo.getResources().getString(R.string.item_music_info), artist.albums, artist.tracks)); - holder.container.setOnClickListener(l -> clickHandler.artistClicked(artist)); + holder.container.setOnClickListener(l -> performClick(artist)); + } + + private void performClick(Artist artist) { + if (clickHandler != null && clickHandler.get() != null) { + clickHandler.get().artistClicked(artist); + } } @Override @@ -58,16 +68,20 @@ public int getItemCount() { return artists.size(); } + public void setClickHandler(@Nullable ArtistsTabsFragment clickHandler) { + this.clickHandler = new WeakReference<>(clickHandler); + } + static class ArtistHolder extends RecyclerView.ViewHolder { - @Bind(R.id.item_container) + @BindView(R.id.item_container) RelativeLayout container; - @Bind(R.id.cover) + @BindView(R.id.cover) ImageView cover; - @Bind(R.id.name) + @BindView(R.id.name) TextView name; - @Bind(R.id.genres) + @BindView(R.id.genres) TextView genres; - @Bind(R.id.music_info) + @BindView(R.id.music_info) TextView musicInfo; public ArtistHolder(View itemView) { diff --git a/app/src/main/res/drawable-hdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-hdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..c9507cf Binary files /dev/null and b/app/src/main/res/drawable-hdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-hdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-hdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..95c6e2f Binary files /dev/null and b/app/src/main/res/drawable-hdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-hdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..6b70e37 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-hdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..19c7e81 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_yamusic.png b/app/src/main/res/drawable-hdpi/ic_yamusic.png new file mode 100755 index 0000000..ea3eeda Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-mdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-mdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..c4a65a8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-mdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-mdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..28d132b Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-mdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..deb07a4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-mdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..7870732 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_yamusic.png b/app/src/main/res/drawable-mdpi/ic_yamusic.png new file mode 100755 index 0000000..b06a63a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_yaradio.png b/app/src/main/res/drawable-mdpi/ic_yaradio.png new file mode 100755 index 0000000..cec8db2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_yaradio.png differ diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xhdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..dae527f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-xhdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..701c0d1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xhdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..176946e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-xhdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..6ae61a8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_yamusic.png b/app/src/main/res/drawable-xhdpi/ic_yamusic.png new file mode 100755 index 0000000..4a906d4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_yaradio.png b/app/src/main/res/drawable-xhdpi/ic_yaradio.png new file mode 100755 index 0000000..83956e2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_yaradio.png differ diff --git a/app/src/main/res/drawable-xxhdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xxhdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..b919e7f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xxhdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-xxhdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..f6a8d6c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xxhdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..296a767 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-xxhdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..108b35e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_yamusic.png b/app/src/main/res/drawable-xxhdpi/ic_yamusic.png new file mode 100755 index 0000000..e67d93b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_yaradio.png b/app/src/main/res/drawable-xxhdpi/ic_yaradio.png new file mode 100755 index 0000000..928db32 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_yaradio.png differ diff --git a/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..84bf4ef Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..6cdb8b8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..44e371e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..a44264b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_yamusic.png b/app/src/main/res/drawable-xxxhdpi/ic_yamusic.png new file mode 100755 index 0000000..87be3f0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_yaradio.png b/app/src/main/res/drawable-xxxhdpi/ic_yaradio.png new file mode 100755 index 0000000..958eb25 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_yaradio.png differ diff --git a/app/src/main/res/drawable/shadow.xml b/app/src/main/res/drawable/shadow.xml new file mode 100644 index 0000000..4567863 --- /dev/null +++ b/app/src/main/res/drawable/shadow.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-xlarge/fragment_artists_tabs.xml b/app/src/main/res/layout-xlarge/fragment_artists_tabs.xml new file mode 100644 index 0000000..070b1bc --- /dev/null +++ b/app/src/main/res/layout-xlarge/fragment_artists_tabs.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 48e82b4..f8e0c7f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,24 +1,35 @@ - + - + android:layout_height="wrap_content" + android:orientation="vertical"> + + + + + + - + android:layout_height="match_parent" + android:layout_marginTop="@dimen/toolbar"/> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_artists_tabs.xml b/app/src/main/res/layout/fragment_artists_tabs.xml new file mode 100644 index 0000000..7909639 --- /dev/null +++ b/app/src/main/res/layout/fragment_artists_tabs.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tab.xml b/app/src/main/res/layout/fragment_tab.xml new file mode 100644 index 0000000..a16e011 --- /dev/null +++ b/app/src/main/res/layout/fragment_tab.xml @@ -0,0 +1,24 @@ + + + + + +