Added Android code master
authorPiotr Ostrowski <p.ostrowski@eastcodes.eu>
Mon, 24 Dec 2018 12:37:02 +0000 (13:37 +0100)
committerPiotr Ostrowski <p.ostrowski@eastcodes.eu>
Mon, 24 Dec 2018 12:37:02 +0000 (13:37 +0100)
963 files changed:
.gitignore [new file with mode: 0644]
Android/app/build.gradle [new file with mode: 0644]
Android/app/google-services.json [new file with mode: 0644]
Android/app/objectbox-models/default.json [new file with mode: 0644]
Android/app/objectbox-models/default.json.bak [new file with mode: 0644]
Android/app/proguard-rules.pro [new file with mode: 0644]
Android/app/src/androidTest/java/com/moiseum/wolnelektury/ExampleInstrumentedTest.java [new file with mode: 0644]
Android/app/src/main/AndroidManifest.xml [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/AbstractIntent.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/DataObserver.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/DataProvider.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/WLApplication.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/FragmentLifecyclePresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/FragmentPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/LifecyclePresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/LoadingView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PaginableLoadingView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/Presenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/base/mvp/PresenterFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/components/CheckableRelativeLayout.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/components/ProgressRecyclerView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/components/ZoomableViewPager.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/EndlessRecyclerOnScrollListener.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/RecyclerAdapter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/components/recycler/ViewHolder.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/ErrorHandler.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClient.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/RestClientCallback.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/WolneLekturyFirebaseMessagingService.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileCacheUtils.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/downloads/FileDownloadIntentService.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/NewApiInterceptor.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/OAuthSigningInterceptor.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/interceptors/UnauthorizedInterceptor.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookDetailsModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/BookModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/CategoryModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FavouriteStateModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/FragmentModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/MediaModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/NewsModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/OAuthTokenModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/ReadingStateModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/models/UserModel.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/BooksService.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/CategoriesService.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/NewsService.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/connection/services/UserService.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/events/BookFavouriteEvent.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/events/LoggedInEvent.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/storage/BookStorage.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/storage/StringListConverter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/utils/SharedPreferencesUtils.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/utils/StringUtils.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/utils/TrackerUtils.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/AboutFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/WebViewFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookType.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/BookView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/components/ProgressDownloadButton.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/AudiobooksDataProvider.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BookListType.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListAdapter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/BooksListView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/DownloadedBooksDataProvider.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/FavouritesDataProvider.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/NewestBooksDataProvider.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/ReadingStateDataProvider.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/book/list/RecommendedBooksDataProvider.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/library/BookViewHolder.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryAdapter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/library/LibraryView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/login/LoginView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/MainView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationAdapter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationBlankViewHolder.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationElement.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/NavigationViewHolder.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SeparatorViewHolder.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/SupportViewHolder.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/main/events/PremiumStatusEvent.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListAdapter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/NewsListView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsGalleryAdapter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/single/NewsView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPhotosAdapter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/news/zoom/ZoomView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/PlayerView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/header/PlayerHeaderView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistAdapter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/playlist/PlayerPlaylistView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/AudiobookLibrary.java [new file with mode: 0755]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/AudiobookService.java [new file with mode: 0755]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaBrowserHelper.java [new file with mode: 0755]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaNotificationManager.java [new file with mode: 0755]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/MediaPlayerAdapter.java [new file with mode: 0755]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/PlaybackInfoListener.java [new file with mode: 0755]
Android/app/src/main/java/com/moiseum/wolnelektury/view/player/service/PlayerAdapter.java [new file with mode: 0755]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchFiltersAdapter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/BookSearchView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/components/EmptySupportRecyclerView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/components/FiltersProgressFlowLayout.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/dto/FilterDto.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/search/filter/FilterView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsFragment.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsPresenter.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/settings/SettingsView.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/splash/SplashActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/supportus/SupportUsActivity.java [new file with mode: 0644]
Android/app/src/main/java/com/moiseum/wolnelektury/view/supportus/SupportUsFragment.java [new file with mode: 0644]
Android/app/src/main/res/color/selector_button_white_border_text_color.xml [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/font_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/font_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_about.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_accept.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_accept_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_book.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_close.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_close_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_comment.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_fav_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_fav_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_fav_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_filter.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_filter_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_glass_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_glass_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_glass_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_menu_all.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_menu_audiobook.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_menu_downloaded.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_menu_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_menu_library.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_menu_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_menu_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_menu_star.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_news.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_notification.png [new file with mode: 0755]
Android/app/src/main/res/drawable-hdpi/ic_notification_player.png [new file with mode: 0755]
Android/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-hdpi/ic_prev.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_reader_dark.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_reader_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_reload.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_search_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_settings.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_share.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_skip_next_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-hdpi/ic_skip_previous_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-hdpi/ic_speaker_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_speaker_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_speaker_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_star_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_star_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_star_small_selected.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_stat_image_audiotrack.png [new file with mode: 0755]
Android/app/src/main/res/drawable-hdpi/ic_toggle.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/ic_trash.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/logo_fnp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/logo_mkidn.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/logo_opp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/logo_wl_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/player_chapter_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/player_chapter_previous.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/player_controls_forward.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/player_controls_pause.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/player_controls_play.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/player_controls_rewind.png [new file with mode: 0644]
Android/app/src/main/res/drawable-hdpi/splash.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/font_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/font_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_about.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_accept.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_accept_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_book.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_close.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_close_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_comment.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_fav_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_fav_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_fav_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_filter.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_filter_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_glass_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_glass_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_glass_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_menu_all.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_menu_audiobook.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_menu_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_menu_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_menu_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_menu_star.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_news.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_notification.png [new file with mode: 0755]
Android/app/src/main/res/drawable-mdpi/ic_notification_player.png [new file with mode: 0755]
Android/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-mdpi/ic_prev.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_reader_dark.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_reader_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_reload.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_search_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_settings.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_share.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_skip_next_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-mdpi/ic_skip_previous_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-mdpi/ic_speaker_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_speaker_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_speaker_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_star_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_star_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_star_small_selected.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_stat_image_audiotrack.png [new file with mode: 0755]
Android/app/src/main/res/drawable-mdpi/ic_toggle.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/ic_trash.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/logo_fnp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/logo_mkidn.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/logo_opp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/logo_wl_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/player_chapter_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/player_chapter_previous.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/player_controls_forward.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/player_controls_pause.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/player_controls_play.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/player_controls_rewind.png [new file with mode: 0644]
Android/app/src/main/res/drawable-mdpi/splash.png [new file with mode: 0644]
Android/app/src/main/res/drawable-nodpi/album_jazz_blues.jpg [new file with mode: 0755]
Android/app/src/main/res/drawable-nodpi/album_youtube_audio_library_rock_2.jpg [new file with mode: 0755]
Android/app/src/main/res/drawable-nodpi/list_nocover.png [new file with mode: 0644]
Android/app/src/main/res/drawable-v21/nav_item_background_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/font_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/font_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_about.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_accept.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_accept_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_book.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_close.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_close_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_comment.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_fav_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_fav_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_fav_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_filter.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_filter_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_glass_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_glass_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_glass_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_menu_all.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_menu_audiobook.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_menu_downloaded.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_menu_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_menu_library.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_menu_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_menu_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_menu_star.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_news.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_notification.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xhdpi/ic_notification_player.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xhdpi/ic_prev.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_reader_dark.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_reader_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_reload.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_search_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_settings.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_share.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xhdpi/ic_speaker_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_speaker_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_speaker_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_star_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_star_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_star_small_selected.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_stat_image_audiotrack.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xhdpi/ic_toggle.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/ic_trash.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/logo_fnp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/logo_mkidn.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/logo_opp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/logo_wl_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/player_chapter_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/player_chapter_previous.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/player_controls_forward.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/player_controls_pause.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/player_controls_play.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/player_controls_rewind.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xhdpi/splash.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/font_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/font_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_about.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_accept.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_accept_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_book.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_close.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_close_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_comment.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_fav_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_fav_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_fav_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_filter.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_filter_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_glass_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_glass_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_glass_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_media_with_pause.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxhdpi/ic_media_with_play.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxhdpi/ic_menu_all.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_menu_audiobook.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_menu_downloaded.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_menu_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_menu_library.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_menu_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_menu_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_menu_star.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_news.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_notification.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxhdpi/ic_notification_player.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxhdpi/ic_prev.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_reader_dark.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_reader_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_reload.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_search_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_settings.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_share.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxhdpi/ic_speaker_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_speaker_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_speaker_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_star_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_star_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_star_small_selected.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_stat_image_audiotrack.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxhdpi/ic_toggle.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/ic_trash.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/logo_fnp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/logo_mkidn.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/logo_opp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/logo_wl_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/player_chapter_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/player_chapter_previous.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/player_controls_forward.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/player_controls_pause.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/player_controls_play.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/player_controls_rewind.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxhdpi/splash.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/font_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/font_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_about.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_accept.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_accept_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_book.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_close.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_close_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_comment.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_fav_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_fav_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_fav_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_filter.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_filter_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_glass_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_glass_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_glass_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_menu_all.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_menu_audiobook.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_menu_downloaded.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_menu_fav.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_menu_library.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_menu_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_menu_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_menu_star.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_news.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_notification.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxxhdpi/ic_notification_player.png [new file with mode: 0755]
Android/app/src/main/res/drawable-xxxhdpi/ic_prev.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_reader_dark.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_reader_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_reload.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_search.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_search_new.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_settings.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_share.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_big.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_speaker_small.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_star_big_active.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_star_mid.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_star_small_selected.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_toggle.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/ic_trash.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/logo_fnp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/logo_mkidn.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/logo_opp.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/logo_wl_light.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/player_chapter_next.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/player_chapter_previous.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/player_controls_forward.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/player_controls_pause.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/player_controls_play.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/player_controls_rewind.png [new file with mode: 0644]
Android/app/src/main/res/drawable-xxxhdpi/splash.png [new file with mode: 0644]
Android/app/src/main/res/drawable/accept_orange_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/accept_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/background_geen.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/close_filters_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/close_small_dark_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/close_small_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/delete_icon_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/filter_background.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/filter_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_arrow_back_white_24dp.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_arrow_forward_white_24dp.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_arrow_right_24dp.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_arrow_right_white_24dp.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_fav_tint_orange_light.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_glass_mid_tint_white.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_launcher_background.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_menu_star_tint_white.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_next_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_prev_gray_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_prev_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_refresh_white_24dp.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_speaker_mid_tint_white.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/ic_todo.png [new file with mode: 0644]
Android/app/src/main/res/drawable/nav_item_background.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/nav_item_background_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/next_con_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/next_icon_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/next_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/orange_details_round_rect.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/orange_round_rect.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/pause_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/pause_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/play_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/play_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/play_white_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/prev_con_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/prev_con_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/prev_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/progress_background.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/refresh_active_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/refresh_icon_light_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/refresh_icon_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/refresh_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/refresh_tint_white.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/round_background_overlay.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/search_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/seekbar_progress.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/seekbar_thumb_color.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/selector_button_white_border.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/toggle_selector.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/toggle_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/trash_active_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/trash_tint.xml [new file with mode: 0644]
Android/app/src/main/res/drawable/white_star.xml [new file with mode: 0644]
Android/app/src/main/res/layout-v21/activity_login.xml [new file with mode: 0644]
Android/app/src/main/res/layout-v21/fragment_player_header.xml [new file with mode: 0644]
Android/app/src/main/res/layout/activity_blank.xml [new file with mode: 0644]
Android/app/src/main/res/layout/activity_login.xml [new file with mode: 0644]
Android/app/src/main/res/layout/activity_main.xml [new file with mode: 0644]
Android/app/src/main/res/layout/activity_splash.xml [new file with mode: 0644]
Android/app/src/main/res/layout/book_item.xml [new file with mode: 0644]
Android/app/src/main/res/layout/checkbox.xml [new file with mode: 0644]
Android/app/src/main/res/layout/filter_item.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_about.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_book.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_book_details.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_book_header.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_books_list.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_filter.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_library.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_news.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_player.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_player_controls.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_player_header.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_player_playlist.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_search.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_settings.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_single_news.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_single_news_gallery_item.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_single_news_header.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_support_us.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_web_view.xml [new file with mode: 0644]
Android/app/src/main/res/layout/fragment_zoom.xml [new file with mode: 0644]
Android/app/src/main/res/layout/library_header.xml [new file with mode: 0644]
Android/app/src/main/res/layout/list_search.xml [new file with mode: 0644]
Android/app/src/main/res/layout/navigation_blank.xml [new file with mode: 0644]
Android/app/src/main/res/layout/navigation_footer.xml [new file with mode: 0644]
Android/app/src/main/res/layout/navigation_item.xml [new file with mode: 0644]
Android/app/src/main/res/layout/navigation_separator_item.xml [new file with mode: 0644]
Android/app/src/main/res/layout/navigation_support_item.xml [new file with mode: 0644]
Android/app/src/main/res/layout/news_item.xml [new file with mode: 0644]
Android/app/src/main/res/layout/playlist_item.xml [new file with mode: 0644]
Android/app/src/main/res/layout/prapremiere_info.xml [new file with mode: 0644]
Android/app/src/main/res/layout/progress_flowlayout.xml [new file with mode: 0644]
Android/app/src/main/res/layout/progress_recyclerview.xml [new file with mode: 0644]
Android/app/src/main/res/layout/zoom_item.xml [new file with mode: 0644]
Android/app/src/main/res/menu/menu_filter.xml [new file with mode: 0644]
Android/app/src/main/res/menu/menu_search.xml [new file with mode: 0644]
Android/app/src/main/res/menu/menu_searchable.xml [new file with mode: 0644]
Android/app/src/main/res/mipmap-hdpi/ic_launcher.png [new file with mode: 0755]
Android/app/src/main/res/mipmap-mdpi/ic_launcher.png [new file with mode: 0755]
Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png [new file with mode: 0755]
Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png [new file with mode: 0755]
Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png [new file with mode: 0755]
Android/app/src/main/res/values-v21/styles.xml [new file with mode: 0644]
Android/app/src/main/res/values/attrs.xml [new file with mode: 0644]
Android/app/src/main/res/values/colors.xml [new file with mode: 0644]
Android/app/src/main/res/values/dimens.xml [new file with mode: 0644]
Android/app/src/main/res/values/strings.xml [new file with mode: 0644]
Android/app/src/main/res/values/styles.xml [new file with mode: 0644]
Android/app/src/test/java/com/moiseum/wolnelektury/ExampleUnitTest.java [new file with mode: 0644]
Android/build.gradle [new file with mode: 0644]
Android/config/quality/checkstyle/checkstyle-config.xml [new file with mode: 0755]
Android/config/quality/findbugs/android-exclude-filter.xml [new file with mode: 0755]
Android/config/quality/pmd/pmd-ruleset.xml [new file with mode: 0755]
Android/config/quality/quality.gradle [new file with mode: 0755]
Android/folioreader/AndroidManifest.xml [new file with mode: 0755]
Android/folioreader/bintray/bintrayv1.gradle [new file with mode: 0755]
Android/folioreader/bintray/installv1.gradle [new file with mode: 0755]
Android/folioreader/build.gradle [new file with mode: 0755]
Android/folioreader/libs/epublib-core-latest.jar [new file with mode: 0755]
Android/folioreader/libs/slf4j-android-1.5.8.jar [new file with mode: 0755]
Android/folioreader/res/anim/disappear.xml [new file with mode: 0755]
Android/folioreader/res/anim/enter_from_left.xml [new file with mode: 0755]
Android/folioreader/res/anim/enter_from_right.xml [new file with mode: 0755]
Android/folioreader/res/anim/exit_to_left.xml [new file with mode: 0755]
Android/folioreader/res/anim/exit_to_right.xml [new file with mode: 0755]
Android/folioreader/res/anim/fadein.xml [new file with mode: 0755]
Android/folioreader/res/anim/fadeout.xml [new file with mode: 0755]
Android/folioreader/res/anim/grow_from_bottom.xml [new file with mode: 0755]
Android/folioreader/res/anim/grow_from_bottomleft_to_topright.xml [new file with mode: 0755]
Android/folioreader/res/anim/grow_from_bottomright_to_topleft.xml [new file with mode: 0755]
Android/folioreader/res/anim/grow_from_top.xml [new file with mode: 0755]
Android/folioreader/res/anim/grow_from_topleft_to_bottomright.xml [new file with mode: 0755]
Android/folioreader/res/anim/grow_from_topright_to_bottomleft.xml [new file with mode: 0755]
Android/folioreader/res/anim/pump_bottom.xml [new file with mode: 0755]
Android/folioreader/res/anim/pump_top.xml [new file with mode: 0755]
Android/folioreader/res/anim/shrink_from_bottom.xml [new file with mode: 0755]
Android/folioreader/res/anim/shrink_from_bottomleft_to_topright.xml [new file with mode: 0755]
Android/folioreader/res/anim/shrink_from_bottomright_to_topleft.xml [new file with mode: 0755]
Android/folioreader/res/anim/shrink_from_top.xml [new file with mode: 0755]
Android/folioreader/res/anim/shrink_from_topleft_to_bottomright.xml [new file with mode: 0755]
Android/folioreader/res/anim/shrink_from_topright_to_bottomleft.xml [new file with mode: 0755]
Android/folioreader/res/anim/slide_down.xml [new file with mode: 0755]
Android/folioreader/res/anim/slide_in_up.xml [new file with mode: 0755]
Android/folioreader/res/anim/slide_out_up.xml [new file with mode: 0755]
Android/folioreader/res/anim/slide_up.xml [new file with mode: 0755]
Android/folioreader/res/color/content_highlight_text_selector_night_mode.xml [new file with mode: 0755]
Android/folioreader/res/drawable-hdpi/font_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-hdpi/font_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-hdpi/ic_comment.png [new file with mode: 0644]
Android/folioreader/res/drawable-hdpi/ic_menu_all.png [new file with mode: 0644]
Android/folioreader/res/drawable-hdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/drawable-hdpi/inset_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-hdpi/inset_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-hdpi/margin_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-hdpi/margin_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-mdpi/font_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-mdpi/font_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-mdpi/ic_comment.png [new file with mode: 0644]
Android/folioreader/res/drawable-mdpi/ic_menu_all.png [new file with mode: 0644]
Android/folioreader/res/drawable-mdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/drawable-mdpi/inset_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-mdpi/inset_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-mdpi/margin_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-mdpi/margin_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-xhdpi/colors_marker.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/edit_note.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/font_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-xhdpi/font_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-xhdpi/ic_action_discard.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/ic_action_share.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/ic_blue_marker.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/ic_comment.png [new file with mode: 0644]
Android/folioreader/res/drawable-xhdpi/ic_drawer.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/ic_green_marker.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/ic_menu_all.png [new file with mode: 0644]
Android/folioreader/res/drawable-xhdpi/ic_pink_marker.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/ic_underline_marker.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/ic_yellow_marker.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/icon_close.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/icon_font_big.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/icon_font_small.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/icon_moon_normal.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/icon_moon_sel.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/icon_sun_normal.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/icon_sun_sel.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/inset_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-xhdpi/inset_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-xhdpi/man_speech_icon.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/margin_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-xhdpi/margin_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-xhdpi/next_icon.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/pause_btn.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/play_icon.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/prev_con.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/seekbar_thumb.png [new file with mode: 0755]
Android/folioreader/res/drawable-xhdpi/trash.png [new file with mode: 0755]
Android/folioreader/res/drawable-xxhdpi/font_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxhdpi/font_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxhdpi/ic_comment.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxhdpi/ic_menu_all.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxhdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/drawable-xxhdpi/inset_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxhdpi/inset_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxhdpi/margin_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxhdpi/margin_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxxhdpi/font_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxxhdpi/font_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxxhdpi/ic_comment.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxxhdpi/ic_menu_all.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxxhdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/drawable-xxxhdpi/inset_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxxhdpi/inset_small.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxxhdpi/margin_big.png [new file with mode: 0644]
Android/folioreader/res/drawable-xxxhdpi/margin_small.png [new file with mode: 0644]
Android/folioreader/res/drawable/arrow_down.png [new file with mode: 0755]
Android/folioreader/res/drawable/arrow_up.png [new file with mode: 0755]
Android/folioreader/res/drawable/btn_contents_highlights.xml [new file with mode: 0755]
Android/folioreader/res/drawable/btn_moon_selector.xml [new file with mode: 0755]
Android/folioreader/res/drawable/btn_sun_selector.xml [new file with mode: 0755]
Android/folioreader/res/drawable/content_highlight_back_selector_night_mode.xml [new file with mode: 0755]
Android/folioreader/res/drawable/content_highlight_text_selector.xml [new file with mode: 0755]
Android/folioreader/res/drawable/dottet_line.xml [new file with mode: 0755]
Android/folioreader/res/drawable/font_text_selector.xml [new file with mode: 0755]
Android/folioreader/res/drawable/ic_close_green_24dp.xml [new file with mode: 0755]
Android/folioreader/res/drawable/ic_drawer_green_24dp.xml [new file with mode: 0755]
Android/folioreader/res/drawable/ic_minus_black_24dp.xml [new file with mode: 0755]
Android/folioreader/res/drawable/ic_minus_white_24dp.xml [new file with mode: 0755]
Android/folioreader/res/drawable/ic_offline_gray_48dp.xml [new file with mode: 0755]
Android/folioreader/res/drawable/ic_plus_black_24dp.xml [new file with mode: 0755]
Android/folioreader/res/drawable/ic_plus_white_24dp.xml [new file with mode: 0755]
Android/folioreader/res/drawable/ic_volume_gray_24dp.xml [new file with mode: 0755]
Android/folioreader/res/drawable/icons_sroll.png [new file with mode: 0755]
Android/folioreader/res/drawable/note_edittext_background.xml [new file with mode: 0755]
Android/folioreader/res/drawable/popup.9.png [new file with mode: 0755]
Android/folioreader/res/drawable/round_button.xml [new file with mode: 0755]
Android/folioreader/res/drawable/style_back_color_selector.xml [new file with mode: 0755]
Android/folioreader/res/drawable/style_text_color_selector.xml [new file with mode: 0755]
Android/folioreader/res/drawable/thumb.xml [new file with mode: 0755]
Android/folioreader/res/drawable/transparent_selector.xml [new file with mode: 0755]
Android/folioreader/res/layout/action_item_horizontal.xml [new file with mode: 0755]
Android/folioreader/res/layout/action_item_vertical.xml [new file with mode: 0755]
Android/folioreader/res/layout/activity_content_highlight.xml [new file with mode: 0755]
Android/folioreader/res/layout/dialog_edit_notes.xml [new file with mode: 0755]
Android/folioreader/res/layout/folio_activity.xml [new file with mode: 0755]
Android/folioreader/res/layout/folio_page_fragment.xml [new file with mode: 0755]
Android/folioreader/res/layout/fragment_contents.xml [new file with mode: 0755]
Android/folioreader/res/layout/fragment_highlight_list.xml [new file with mode: 0755]
Android/folioreader/res/layout/horiz_separator.xml [new file with mode: 0755]
Android/folioreader/res/layout/item_dictionary.xml [new file with mode: 0755]
Android/folioreader/res/layout/layout_dictionary.xml [new file with mode: 0755]
Android/folioreader/res/layout/layout_wikipedia.xml [new file with mode: 0755]
Android/folioreader/res/layout/popup_horizontal.xml [new file with mode: 0755]
Android/folioreader/res/layout/popup_vertical.xml [new file with mode: 0755]
Android/folioreader/res/layout/progress_dialog.xml [new file with mode: 0755]
Android/folioreader/res/layout/row_font.xml [new file with mode: 0755]
Android/folioreader/res/layout/row_highlight.xml [new file with mode: 0755]
Android/folioreader/res/layout/row_table_of_contents.xml [new file with mode: 0755]
Android/folioreader/res/layout/view_audio_player.xml [new file with mode: 0755]
Android/folioreader/res/layout/view_config.xml [new file with mode: 0755]
Android/folioreader/res/menu/context_menu.xml [new file with mode: 0755]
Android/folioreader/res/menu/menu_on_highlight.xml [new file with mode: 0755]
Android/folioreader/res/menu/menu_text_selection.xml [new file with mode: 0755]
Android/folioreader/res/mipmap-hdpi/ic_launcher.png [new file with mode: 0755]
Android/folioreader/res/mipmap-mdpi/ic_launcher.png [new file with mode: 0755]
Android/folioreader/res/mipmap-xhdpi/ic_launcher.png [new file with mode: 0755]
Android/folioreader/res/mipmap-xxhdpi/ic_launcher.png [new file with mode: 0755]
Android/folioreader/res/res/drawable-hdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/res/drawable-mdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/res/drawable-xhdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/res/drawable-xxhdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/res/drawable-xxxhdpi/icon_font.png [new file with mode: 0755]
Android/folioreader/res/values-w820dp/dimens.xml [new file with mode: 0755]
Android/folioreader/res/values/attrs.xml [new file with mode: 0755]
Android/folioreader/res/values/colors.xml [new file with mode: 0755]
Android/folioreader/res/values/dimens.xml [new file with mode: 0755]
Android/folioreader/res/values/strings.xml [new file with mode: 0755]
Android/folioreader/res/values/styles.xml [new file with mode: 0755]
Android/folioreader/src/main/assets/css/Style.css [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/andada/Andada-Bold.otf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/andada/Andada-BoldItalic.otf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/andada/Andada-Italic.otf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/andada/Andada-Regular.otf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Bold.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-BoldItalic.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Italic.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Regular.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/lato/Lato-Bold.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/lato/Lato-BoldItalic.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/lato/Lato-Italic.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/lato/Lato-Regular.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/lora/Lora-Bold.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/lora/Lora-BoldItalic.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/lora/Lora-Italic.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/lora/Lora-Regular.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/raleway/Raleway-Bold.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/raleway/Raleway-BoldItalic.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/raleway/Raleway-Italic.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/fonts/raleway/Raleway-Regular.ttf [new file with mode: 0755]
Android/folioreader/src/main/assets/js/Bridge.js [new file with mode: 0755]
Android/folioreader/src/main/assets/js/jquery-3.1.1.min.js [new file with mode: 0755]
Android/folioreader/src/main/assets/js/jsface.min.js [new file with mode: 0755]
Android/folioreader/src/main/assets/js/rangy-classapplier.js [new file with mode: 0755]
Android/folioreader/src/main/assets/js/rangy-core.js [new file with mode: 0755]
Android/folioreader/src/main/assets/js/rangy-highlighter.js [new file with mode: 0755]
Android/folioreader/src/main/assets/js/rangy-serializer.js [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/Config.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/Constants.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/Font.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/HighLight.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/HighlightImpl.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/TOCLinkWrapper.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/dictionary/Audio.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/dictionary/Dictionary.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/dictionary/DictionaryResults.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/dictionary/Example.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/dictionary/Pronunciations.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/dictionary/Senses.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/dictionary/Wikipedia.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/event/AnchorIdEvent.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/event/BusOwner.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/event/MediaOverlayHighlightStyleEvent.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/event/MediaOverlayPlayPauseEvent.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/event/MediaOverlaySpeedEvent.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/event/ReloadDataEvent.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/event/RewindIndexEvent.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/event/WebViewPosition.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/media_overlay/OverlayItems.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/quickaction/ActionItem.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/quickaction/PopupWindows.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/quickaction/QuickAction.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/sqlite/DbAdapter.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/sqlite/DictionaryTable.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/sqlite/FolioDatabaseHelper.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/model/sqlite/HighLightTable.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/BaseMvpView.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/DictionaryCallBack.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/DictionaryTask.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/HtmlTask.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/HtmlTaskCallback.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/HtmlUtil.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/ManifestCallBack.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/ManifestTask.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/OnSaveHighlight.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/SaveReceivedHighlightTask.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/WikipediaCallBack.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/base/WikipediaTask.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/activity/ContentHighlightActivity.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/activity/FolioActivity.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/activity/ToolbarUtils.java [new file with mode: 0644]
Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/DictionaryAdapter.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/FolioPageFragmentAdapter.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/FontAdapter.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/HighlightAdapter.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/fragment/DictionaryFragment.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/fragment/FolioPageFragment.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/fragment/HighlightFragment.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/mediaoverlay/MediaController.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/mediaoverlay/MediaControllerCallbacks.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/presenter/MainMvpView.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/folio/presenter/MainPresenter.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/adapter/TOCAdapter.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/presenter/TOCMvpView.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/presenter/TableOfContentsPresenter.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/view/TableOfContentFragment.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/AppUtil.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/FileUtil.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/FolioReader.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/HighlightUtil.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/MultiLevelExpIndListAdapter.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/OnHighlightListener.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/ProgressDialog.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/SMILParser.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/ScreenUtils.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/SharedPreferenceUtil.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/StyleableTextView.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/util/UiUtil.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/view/ConfigBottomSheetDialogFragment.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/view/DirectionalViewpager.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/view/ObservableWebView.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/view/StyleableTextView.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/view/UnderlinedTextView.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/view/VerticalSeekbar.java [new file with mode: 0755]
Android/folioreader/src/main/java/com/folioreader/view/VerticalViewPager.java [new file with mode: 0755]
Android/gradle.properties [new file with mode: 0644]
Android/gradle/wrapper/gradle-wrapper.jar [new file with mode: 0644]
Android/gradle/wrapper/gradle-wrapper.properties [new file with mode: 0644]
Android/gradlew [new file with mode: 0755]
Android/gradlew.bat [new file with mode: 0644]
Android/r2-streamer/.gitignore [new file with mode: 0755]
Android/r2-streamer/License.txt [new file with mode: 0755]
Android/r2-streamer/bintray/bintrayv1.gradle [new file with mode: 0755]
Android/r2-streamer/bintray/installv1.gradle [new file with mode: 0755]
Android/r2-streamer/build.gradle [new file with mode: 0755]
Android/r2-streamer/config/quality/checkstyle/checkstyle-config.xml [new file with mode: 0755]
Android/r2-streamer/config/quality/findbugs/android-exclude-filter.xml [new file with mode: 0755]
Android/r2-streamer/config/quality/pmd/pmd-ruleset.xml [new file with mode: 0755]
Android/r2-streamer/config/quality/quality.gradle [new file with mode: 0755]
Android/r2-streamer/gradle.properties [new file with mode: 0755]
Android/r2-streamer/gradle/wrapper/gradle-wrapper.jar [new file with mode: 0755]
Android/r2-streamer/gradle/wrapper/gradle-wrapper.properties [new file with mode: 0755]
Android/r2-streamer/gradlew [new file with mode: 0755]
Android/r2-streamer/gradlew.bat [new file with mode: 0755]
Android/r2-streamer/r2-fetcher/.gitignore [new file with mode: 0755]
Android/r2-streamer/r2-fetcher/build.gradle [new file with mode: 0755]
Android/r2-streamer/r2-fetcher/src/main/java/org/readium/r2_streamer/fetcher/EpubFetcher.java [new file with mode: 0755]
Android/r2-streamer/r2-fetcher/src/main/java/org/readium/r2_streamer/fetcher/EpubFetcherException.java [new file with mode: 0755]
Android/r2-streamer/r2-fetcher/src/main/java/org/readium/r2_streamer/fetcher/Fetcher.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/.gitignore [new file with mode: 0755]
Android/r2-streamer/r2-parser/build.gradle [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/container/Container.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/container/DirectoryContainer.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/container/EpubContainer.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/Encryption.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/EpubPublication.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/Clip.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/MediaOverlayNode.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/MediaOverlays.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/SMILParser.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/contributor/Contributor.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/link/Link.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/metadata/MetaData.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/metadata/MetadataItem.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/Rendition.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionFlow.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionLayout.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionOrientation.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionSpread.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/subject/Subject.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/searcher/SearchQueryResults.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/searcher/SearchResult.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/tableofcontents/TOCLink.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/CBZParser.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EncryptionDecoder.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EncryptionParser.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EpubParser.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EpubParserException.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/MediaOverlayParser.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/NCXParser.java [new file with mode: 0755]
Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/OPFParser.java [new file with mode: 0755]
Android/r2-streamer/r2-server/.gitignore [new file with mode: 0755]
Android/r2-streamer/r2-server/build.gradle [new file with mode: 0755]
Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/EpubServer.java [new file with mode: 0755]
Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/EpubServerSingleton.java [new file with mode: 0755]
Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/ResponseStatus.java [new file with mode: 0755]
Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/ManifestHandler.java [new file with mode: 0755]
Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/MediaOverlayHandler.java [new file with mode: 0755]
Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/ResourceHandler.java [new file with mode: 0755]
Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/SearchQueryHandler.java [new file with mode: 0755]
Android/r2-streamer/sample/.gitignore [new file with mode: 0755]
Android/r2-streamer/sample/build.gradle [new file with mode: 0755]
Android/r2-streamer/sample/proguard-rules.pro [new file with mode: 0755]
Android/r2-streamer/sample/src/androidTest/java/org/readium/sample/ExampleInstrumentedTest.java [new file with mode: 0755]
Android/r2-streamer/sample/src/main/AndroidManifest.xml [new file with mode: 0755]
Android/r2-streamer/sample/src/main/assets/AlmaTademaPortfolio.cbz [new file with mode: 0755]
Android/r2-streamer/sample/src/main/assets/SmokeTestFXL.epub [new file with mode: 0755]
Android/r2-streamer/sample/src/main/java/org/readium/sample/Constant.java [new file with mode: 0755]
Android/r2-streamer/sample/src/main/java/org/readium/sample/TestActivity.java [new file with mode: 0755]
Android/r2-streamer/sample/src/main/java/org/readium/sample/adapters/SearchListAdapter.java [new file with mode: 0755]
Android/r2-streamer/sample/src/main/java/org/readium/sample/adapters/SpineListAdapter.java [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/layout/activity_sample_main.xml [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/layout/searchlist_adapter_resource.xml [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/layout/spinelist_adapter_resource.xml [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/mipmap-hdpi/ic_launcher.png [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/mipmap-mdpi/ic_launcher.png [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/mipmap-xhdpi/ic_launcher.png [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/values-w820dp/dimens.xml [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/values/colors.xml [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/values/dimens.xml [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/values/strings.xml [new file with mode: 0755]
Android/r2-streamer/sample/src/main/res/values/styles.xml [new file with mode: 0755]
Android/r2-streamer/sample/src/test/java/org/readium/sample/ExampleUnitTest.java [new file with mode: 0755]
Android/r2-streamer/settings.gradle [new file with mode: 0755]
Android/settings.gradle [new file with mode: 0644]
Android/webViewMarker/build.gradle [new file with mode: 0755]
Android/webViewMarker/src/main/AndroidManifest.xml [new file with mode: 0755]
Android/webViewMarker/src/main/assets/android.selection.js [new file with mode: 0755]
Android/webViewMarker/src/main/assets/content.html [new file with mode: 0755]
Android/webViewMarker/src/main/assets/css/sample.css [new file with mode: 0755]
Android/webViewMarker/src/main/assets/jpntext.js [new file with mode: 0755]
Android/webViewMarker/src/main/assets/jquery-1.8.3.js [new file with mode: 0755]
Android/webViewMarker/src/main/assets/rangy-core.js [new file with mode: 0755]
Android/webViewMarker/src/main/assets/rangy-serializer.js [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/blahti/drag/DragController.java [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/blahti/drag/DragLayer.java [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/blahti/drag/DragListener.java [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/blahti/drag/DragSource.java [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/blahti/drag/DragView.java [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/blahti/drag/DropTarget.java [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/blahti/drag/MyAbsoluteLayout.java [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/bossturban/webviewmarker/TextSelectionControlListener.java [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/bossturban/webviewmarker/TextSelectionController.java [new file with mode: 0755]
Android/webViewMarker/src/main/java/com/bossturban/webviewmarker/TextSelectionSupport.java [new file with mode: 0755]
Android/webViewMarker/src/main/res/drawable-hdpi/text_select_handle_left.png [new file with mode: 0755]
Android/webViewMarker/src/main/res/drawable-hdpi/text_select_handle_right.png [new file with mode: 0755]
Android/webViewMarker/src/main/res/drawable-mdpi/text_select_handle_left.png [new file with mode: 0755]
Android/webViewMarker/src/main/res/drawable-mdpi/text_select_handle_right.png [new file with mode: 0755]
Android/webViewMarker/src/main/res/drawable-xhdpi/text_select_handle_left.png [new file with mode: 0755]
Android/webViewMarker/src/main/res/drawable-xhdpi/text_select_handle_right.png [new file with mode: 0755]
Android/webViewMarker/src/main/res/layout/selection_drag_layer.xml [new file with mode: 0755]
Android/webViewMarker/src/main/res/values/strings.xml [new file with mode: 0755]
Android/webViewMarker/src/main/res/values/styles.xml [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..f3129f9
--- /dev/null
@@ -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 (file)
index 0000000..c17a047
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/Android/app/objectbox-models/default.json b/Android/app/objectbox-models/default.json
new file mode 100644 (file)
index 0000000..64de239
--- /dev/null
@@ -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 (file)
index 0000000..15b567d
--- /dev/null
@@ -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 (file)
index 0000000..f1b4245
--- /dev/null
@@ -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 (file)
index 0000000..35def5e
--- /dev/null
@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@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 (file)
index 0000000..a9e83c0
--- /dev/null
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="com.moiseum.wolnelektury"
+          xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+    <application
+        android:name=".base.WLApplication"
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/WLAppTheme">
+        <activity
+            android:name=".view.splash.SplashActivity"
+            android:screenOrientation="userPortrait"
+            android:theme="@style/WLAppTheme.NoActionBar">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".view.main.MainActivity"
+            android:launchMode="singleInstance"
+            android:screenOrientation="userPortrait">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+
+                <data
+                    android:host="oauth.callback"
+                    android:scheme="wolnelekturyapp"/>
+
+                <data
+                    android:host="paypal_return"
+                    android:scheme="wolnelekturyapp"/>
+
+                <data
+                    android:host="paypal_error"
+                    android:scheme="wolnelekturyapp"/>
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".view.book.BookActivity"
+            android:screenOrientation="userPortrait"
+            android:theme="@style/WLAppTheme.NoActionBar"/>
+        <activity
+            android:name=".view.player.PlayerActivity"
+            android:screenOrientation="userPortrait"
+            android:theme="@style/WLAppTheme.NoActionBar"/>
+        <activity
+            android:name=".view.search.filter.FilterActivity"
+            android:screenOrientation="userPortrait"/>
+        <activity
+            android:name="com.folioreader.ui.folio.activity.FolioActivity"
+            android:configChanges="orientation|screenSize"
+            android:label="@string/app_name"
+            android:theme="@style/WLAppTheme.NoActionBar"/>
+        <activity
+            android:name=".view.WebViewActivity"
+            android:screenOrientation="userPortrait"/>
+        <activity
+            android:name=".view.supportus.SupportUsActivity"
+            android:screenOrientation="userPortrait"/>
+        <activity
+            android:name=".view.book.list.BookListActivity"
+            android:screenOrientation="userPortrait"/>
+        <activity
+            android:name=".view.news.single.NewsActivity"
+            android:screenOrientation="userPortrait"
+            android:theme="@style/WLAppTheme.NoActionBar"/>
+        <activity
+            android:name=".view.news.zoom.ZoomActivity"
+            android:screenOrientation="userPortrait"
+            android:theme="@style/WLAppTheme.NoActionBar"/>
+        <activity
+            android:name=".view.login.LoginActivity"
+            android:screenOrientation="userPortrait"
+            android:theme="@style/WLAppTheme.NoActionBar"/>
+
+        <service
+            android:name=".connection.downloads.FileDownloadIntentService"
+            android:exported="false"/>
+        <service
+            android:name=".view.player.service.AudiobookService"
+            android:enabled="true"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.media.browse.MediaBrowserService"/>
+            </intent-filter>
+        </service>
+        <service
+            android:name=".connection.WolneLekturyFirebaseMessagingService">
+            <intent-filter>
+                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
+            </intent-filter>
+        </service>
+
+        <receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.MEDIA_BUTTON"/>
+            </intent-filter>
+        </receiver>
+
+        <meta-data
+            android:name="io.fabric.ApiKey"
+            android:value=""/>
+        <meta-data
+            android:name="com.google.firebase.messaging.default_notification_icon"
+            android:resource="@drawable/ic_notification"/>
+        <meta-data
+            android:name="com.google.firebase.messaging.default_notification_color"
+            android:resource="@color/colorAccent"/>
+        <meta-data
+            android:name="com.google.firebase.messaging.default_notification_channel_id"
+            android:value="@string/default_notification_channel_id"/>
+
+    </application>
+
+</manifest>
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 (file)
index 0000000..a4c3695
--- /dev/null
@@ -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<ApplicationInfo> 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 (file)
index 0000000..5a0783b
--- /dev/null
@@ -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<ApplicationInfo> 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 (file)
index 0000000..3127710
--- /dev/null
@@ -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 (file)
index 0000000..607a340
--- /dev/null
@@ -0,0 +1,12 @@
+package com.moiseum.wolnelektury.base;
+
+/**
+ * @author golonkos.
+ */
+public interface DataObserver<T> {
+       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 (file)
index 0000000..a0663ba
--- /dev/null
@@ -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<T, S> extends RestClientCallback<T, S> {
+
+       private final static String TAG = DataProvider.class.getSimpleName();
+
+       protected DataObserver<T> dataObserver;
+       protected String lastKeySlug = null;
+       private Call<T> call;
+
+       public DataProvider() {
+       }
+
+       public void setDataObserver(DataObserver<T> 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<S> 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 (file)
index 0000000..c72915b
--- /dev/null
@@ -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 (file)
index 0000000..6b2d897
--- /dev/null
@@ -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 (file)
index 0000000..ac4587a
--- /dev/null
@@ -0,0 +1,21 @@
+package com.moiseum.wolnelektury.base.mvp;
+
+public class FragmentPresenter<V> 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 (file)
index 0000000..e7aa0b5
--- /dev/null
@@ -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 (file)
index 0000000..98c4095
--- /dev/null
@@ -0,0 +1,10 @@
+package com.moiseum.wolnelektury.base.mvp;
+
+public interface LoadingView<T> {
+
+       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 (file)
index 0000000..c7fbb42
--- /dev/null
@@ -0,0 +1,16 @@
+package com.moiseum.wolnelektury.base.mvp;
+
+/**
+ * Created by Piotr Ostrowski on 28.11.2017.
+ */
+
+public interface PaginableLoadingView<T> {
+
+       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 (file)
index 0000000..acad92f
--- /dev/null
@@ -0,0 +1,24 @@
+package com.moiseum.wolnelektury.base.mvp;
+
+/**
+ * Created by Piotr Ostrowski on 13.06.2018.
+ */
+public class Presenter<V> 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 (file)
index 0000000..d235018
--- /dev/null
@@ -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<P extends Presenter> 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 (file)
index 0000000..b7a0dce
--- /dev/null
@@ -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 <P> type of presenter for this fragment.
+ */
+public abstract class PresenterFragment<P extends FragmentPresenter> 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 (file)
index 0000000..5a19829
--- /dev/null
@@ -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 (file)
index 0000000..cccb42a
--- /dev/null
@@ -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<T> 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<T, ?> 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<T, ?> adapter) {
+               this.adapter = adapter;
+               rvList.setAdapter(adapter);
+       }
+
+       public void setItems(List<T> items) {
+               if (adapter == null) {
+                       throw new UnsupportedOperationException("Adapter not set");
+               }
+               adapter.setItems(items);
+               tvEmpty.setVisibility(items.isEmpty() ? VISIBLE : GONE);
+       }
+
+       public void addItems(List<T> 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 (file)
index 0000000..1cfdb52
--- /dev/null
@@ -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 (file)
index 0000000..073dff1
--- /dev/null
@@ -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 (file)
index 0000000..c2b7137
--- /dev/null
@@ -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<T, VH extends ViewHolder> extends RecyclerView.Adapter<VH> {
+
+       /**
+        * On click listener.
+        */
+       public interface OnItemClickListener<T> {
+               /**
+                * @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<T> onItemClickListener;
+
+       private List<T> 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<T> onItemClickListener) {
+               this.onItemClickListener = onItemClickListener;
+       }
+
+
+       public OnItemClickListener<T> getOnItemClickListener() {
+               return onItemClickListener;
+       }
+
+       public List<T> getItems() {
+               return items;
+       }
+
+       /**
+        * @param items new items
+        */
+       public void setItems(List<T> items) {
+               this.items = items;
+               notifyDataSetChanged();
+       }
+
+       public void addItems(List<T> 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 (file)
index 0000000..53f2f26
--- /dev/null
@@ -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<T> 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 (file)
index 0000000..a14e4fd
--- /dev/null
@@ -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<T> {
+
+       private static final String TAG = ErrorHandler.class.getSimpleName();
+       private final Response<T> response;
+
+       public ErrorHandler(Response<T> 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<T> 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 (file)
index 0000000..35eb345
--- /dev/null
@@ -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 <T, S> Call<T> call(RestClientCallback<T, S> restClientCallback, Class<S> clazz) {
+               S service = createService(clazz);
+               Call<T> call = restClientCallback.execute(service);
+               call.enqueue(restClientCallback);
+               return call;
+       }
+
+       public <S> S createService(Class<S> 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 (file)
index 0000000..41a1ad5
--- /dev/null
@@ -0,0 +1,44 @@
+package com.moiseum.wolnelektury.connection;
+
+import android.util.Log;
+
+import retrofit2.Call;
+import retrofit2.Response;
+
+public abstract class RestClientCallback<T, S> implements retrofit2.Callback<T> {
+
+       private static final String TAG = RestClientCallback.class.getSimpleName();
+
+       @Override
+       public void onResponse(Call<T> call, Response<T> response) {
+               if (response.isSuccessful()) {
+                       onSuccess(response.body());
+               } else {
+                       try {
+                               ErrorHandler<T> errorHandler = new ErrorHandler<>(response);
+                               errorHandler.handle();
+                       } catch (Exception e) {
+                               onFailure(e);
+                       }
+               }
+       }
+
+       @Override
+       public void onFailure(Call<T> 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<T> 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 (file)
index 0000000..7a27db5
--- /dev/null
@@ -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 (file)
index 0000000..dc14253
--- /dev/null
@@ -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<String> 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 (file)
index 0000000..742f0c7
--- /dev/null
@@ -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<String> 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<String> 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<ResponseBody> call = booksService.downloadFileWithUrl(fileUrl);
+                       Response<ResponseBody> response = call.execute();
+                       if (response.isSuccessful()) {
+                               boolean result = FileCacheUtils.writeResponseBodyToDiskCache(response.body(), fileUrl);
+                               EventBus.getDefault().post(new DownloadFileEvent(fileUrl, result));
+                       } else {
+                               ErrorHandler<ResponseBody> 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 (file)
index 0000000..2e874eb
--- /dev/null
@@ -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 (file)
index 0000000..0fc1092
--- /dev/null
@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> parameters) throws IOException {
+               Buffer base = new Buffer();
+               base.writeUtf8(method);
+               base.writeByte('&');
+               base.writeUtf8(utf8(baseUrl));
+               base.writeByte('&');
+
+               boolean first = true;
+               for (Map.Entry<String, String> 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 (file)
index 0000000..91dc357
--- /dev/null
@@ -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 (file)
index 0000000..0a2959e
--- /dev/null
@@ -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<CategoryModel> genres;
+       private List<CategoryModel> kinds;
+       private BookModel parent;
+       private String title;
+       private String url;
+       private List<MediaModel> media;
+       @SerializedName("simple_cover")
+       private String cover;
+       private List<CategoryModel> epochs;
+       private List<CategoryModel> 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<CategoryModel> getGenres() {
+               return genres;
+       }
+
+       public void setGenres(List<CategoryModel> genres) {
+               this.genres = genres;
+       }
+
+       public List<CategoryModel> getKinds() {
+               return kinds;
+       }
+
+       public void setKinds(List<CategoryModel> 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<MediaModel> getMedia() {
+               return media;
+       }
+
+       public void setMedia(List<MediaModel> media) {
+               this.media = media;
+       }
+
+       public String getCover() {
+               return cover;
+       }
+
+       public void setCover(String cover) {
+               this.cover = cover;
+       }
+
+       public List<CategoryModel> getEpochs() {
+               return epochs;
+       }
+
+       public void setEpochs(List<CategoryModel> epochs) {
+               this.epochs = epochs;
+       }
+
+       public List<CategoryModel> getAuthors() {
+               return authors;
+       }
+
+       public void setAuthors(List<CategoryModel> 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<MediaModel> getAudiobookMediaModels() {
+               ArrayList<MediaModel> mediaModels = new ArrayList<>();
+               for (MediaModel mediaFile : getMedia()) {
+                       if (MEDIA_TYPE_MP3.equals(mediaFile.getType())) {
+                               mediaModels.add(mediaFile);
+                       }
+               }
+               return mediaModels;
+       }
+
+       public ArrayList<String> getAudiobookFilesUrls() {
+               ArrayList<String> 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 (file)
index 0000000..d5b8519
--- /dev/null
@@ -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<String> 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<String> getAudioFileUrls() {
+               return audioFileUrls;
+       }
+
+       public void setAudioFileUrls(List<String> 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 (file)
index 0000000..a4bf7da
--- /dev/null
@@ -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 (file)
index 0000000..9058aab
--- /dev/null
@@ -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 (file)
index 0000000..6c4c866
--- /dev/null
@@ -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 (file)
index 0000000..8c78e46
--- /dev/null
@@ -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 (file)
index 0000000..f4d7479
--- /dev/null
@@ -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<String> 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<String> getGalleryUrl() {
+               return galleryUrl;
+       }
+
+       public void setGalleryUrl(List<String> 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 (file)
index 0000000..778ee51
--- /dev/null
@@ -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 (file)
index 0000000..698c6d8
--- /dev/null
@@ -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 (file)
index 0000000..7f5c6d1
--- /dev/null
@@ -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 (file)
index 0000000..1657930
--- /dev/null
@@ -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<List<BookModel>> 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<BookDetailsModel> getBookDetails(@Path("slug") String slug);
+
+       @Streaming
+       @GET
+       Call<ResponseBody> downloadFileWithUrl(@Url String fileUrl);
+
+       @Headers("New-Api: true")
+       @GET("newest/")
+       Call<List<BookModel>> getNewest();
+
+       @Headers("New-Api: true")
+       @GET("recommended/")
+       Call<List<BookModel>> getRecommended();
+
+       @Headers("New-Api: true")
+       @GET("audiobooks/")
+       Call<List<BookModel>> getAudiobooks(@Query("after") String lastKey, @Query("count") int count);
+
+       @Headers("Authentication-Required: true")
+       @POST("reading/{slug}/{state}/")
+       Single<ReadingStateModel> setReadingState(@Path("slug") String slug, @Path("state") String state);
+
+       @Headers("Authentication-Required: true")
+       @GET("reading/{slug}/")
+       Single<ReadingStateModel> getReadingState(@Path("slug") String slug);
+
+       @Headers({"Authentication-Required: true", "New-Api: true"})
+       @GET("shelf/{state}/")
+       Call<List<BookModel>> getReadenBooks(@Path("state") String state, @Query("after") String lastKey, @Query("count") int count);
+
+       @Headers("Authentication-Required: true")
+       @POST("like/{slug}/")
+       Single<FavouriteStateModel> setFavouriteState(@Path("slug") String slug, @Query("action") String action);
+
+       @Headers("Authentication-Required: true")
+       @GET("like/{slug}/")
+       Single<FavouriteStateModel> getFavouriteState(@Path("slug") String slug);
+
+       @GET("preview/")
+       Call<List<BookDetailsModel>> getPreview();
+
+       @GET("books/{slug}")
+       Call<BookDetailsModel> getPreviewMockup(@Path("slug") String slug);
+
+       @Headers({"Authentication-Required: true", "New-Api: true"})
+       @GET("shelf/likes/")
+       Call<List<BookModel>> 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 (file)
index 0000000..c51b088
--- /dev/null
@@ -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<List<CategoryModel>> getEpochs(@Query("book_only") boolean bookOnly);
+
+       @GET("genres")
+       Call<List<CategoryModel>> getGenres(@Query("book_only") boolean bookOnly);
+
+       @GET("kinds")
+       Call<List<CategoryModel>> 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 (file)
index 0000000..fa317eb
--- /dev/null
@@ -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<List<NewsModel>> 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 (file)
index 0000000..9ce35a9
--- /dev/null
@@ -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<OAuthTokenModel> requestToken();
+
+       @Headers("Token-Requested: true")
+       @GET("oauth/access_token/")
+       Call<OAuthTokenModel> accessToken();
+
+       @Headers("Authentication-Required: true")
+       @GET("username/")
+       Call<UserModel> 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 (file)
index 0000000..0bd4497
--- /dev/null
@@ -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 (file)
index 0000000..df2cbd7
--- /dev/null
@@ -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 (file)
index 0000000..31d6628
--- /dev/null
@@ -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<BookModel> 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<BookModel> 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<BookModel> 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 (file)
index 0000000..24b45b7
--- /dev/null
@@ -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<List<String>, String> {
+
+       @Override
+       public List<String> convertToEntityProperty(String databaseValue) {
+               if (databaseValue == null) {
+                       return new ArrayList<>();
+               }
+               return Arrays.asList(databaseValue.split(","));
+       }
+
+       @Override
+       public String convertToDatabaseValue(List<String> 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 (file)
index 0000000..6120e64
--- /dev/null
@@ -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 (file)
index 0000000..d890e9a
--- /dev/null
@@ -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<CategoryModel> 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<CategoryModel> 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 (file)
index 0000000..40d694c
--- /dev/null
@@ -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 (file)
index 0000000..e6c1144
--- /dev/null
@@ -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 (file)
index 0000000..1e30d1f
--- /dev/null
@@ -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 (file)
index 0000000..38bc5dc
--- /dev/null
@@ -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 (file)
index 0000000..8f542af
--- /dev/null
@@ -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 (file)
index 0000000..66282c3
--- /dev/null
@@ -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<BookPresenter> 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 (file)
index 0000000..726e7eb
--- /dev/null
@@ -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<BookView> {
+
+       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<String> 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<String> 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<BookDetailsModel> getBookDetails() {
+               Single<ReadingStateModel> readingStateSingle = preferences.isUserLoggedIn() ? booksService.getReadingState(bookSlug) : Single.just(new ReadingStateModel());
+               Single<FavouriteStateModel> favouriteStateModelSingle = preferences.isUserLoggedIn() ? booksService.getFavouriteState(bookSlug) : Single.just(new FavouriteStateModel());
+               Single<BookDetailsModel> 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<String> 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 (file)
index 0000000..fd576be
--- /dev/null
@@ -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 (file)
index 0000000..217f748
--- /dev/null
@@ -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 (file)
index 0000000..5d177d8
--- /dev/null
@@ -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 (file)
index 0000000..f4d0a42
--- /dev/null
@@ -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<List<BookModel>, BooksService> {
+
+    @Override
+    public Call<List<BookModel>> execute(BooksService service) {
+        return service.getAudiobooks(lastKeySlug , RestClient.PAGINATION_LIMIT);
+    }
+
+    @Override
+    protected Class<BooksService> 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 (file)
index 0000000..7b38560
--- /dev/null
@@ -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 (file)
index 0000000..9e2b326
--- /dev/null
@@ -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<List<BookModel>, 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<List<BookModel>, 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<List<BookModel>, 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<List<BookModel>, 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<List<BookModel>, 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<List<BookModel>, 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<List<BookModel>, 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<List<BookModel>, 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 (file)
index 0000000..07e3f7f
--- /dev/null
@@ -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<BookModel, BooksListAdapter.BookViewHolder> {
+
+       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<BookModel> {
+
+               @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 (file)
index 0000000..0bb96fb
--- /dev/null
@@ -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<BooksListPresenter> implements BooksListView {
+
+       private static final String PARAM_LIST_TYPE = "PARAM_LIST_TYPE";
+
+       @BindView(R.id.rvBooksList)
+       ProgressRecyclerView<BookModel> 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<BookModel> 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 (file)
index 0000000..030bbea
--- /dev/null
@@ -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<BooksListView> {
+
+       private static final String TAG = BooksListPresenter.class.getSimpleName();
+
+       private final DataProvider<List<BookModel>, 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<Completable> 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<List<BookModel>> {
+
+               private List<BookModel> matchDownloadedBooks(List<BookModel> books) {
+                       if (bookListType.isDeletable()) {
+                               List<BookModel> merged = new ArrayList<>(books.size());
+                               List<BookModel> 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<BookModel> 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 (file)
index 0000000..c6249f4
--- /dev/null
@@ -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<List<BookModel>> {
+       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 (file)
index 0000000..81b65cf
--- /dev/null
@@ -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<List<BookModel>, BooksService> {
+
+       @Override
+       public void load(String lastKey) {
+               if (dataObserver != null && lastKey == null) {
+                       BookStorage bookStorage = WLApplication.getInstance().getBookStorage();
+                       dataObserver.onLoadSuccess(bookStorage.all());
+               }
+       }
+
+       @Override
+       public Call<List<BookModel>> execute(BooksService service) {
+               return null;
+       }
+
+       @Override
+       protected Class<BooksService> 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 (file)
index 0000000..4634ba6
--- /dev/null
@@ -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<List<BookModel>,BooksService> {
+
+       @Override
+       public Call<List<BookModel>> execute(BooksService service) {
+               return service.getFavourites(lastKeySlug , RestClient.PAGINATION_LIMIT);
+       }
+
+       @Override
+       protected Class<BooksService> 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 (file)
index 0000000..df703e9
--- /dev/null
@@ -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<List<BookModel>, BooksService> {
+
+       @Override
+       public Call<List<BookModel>> execute(BooksService service) {
+               return service.getNewest();
+       }
+
+       @Override
+       protected Class<BooksService> 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 (file)
index 0000000..356c920
--- /dev/null
@@ -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<List<BookModel>, BooksService> {
+
+       private ReadingStateModel.ReadingState state;
+
+       public ReadingStateDataProvider(ReadingStateModel.ReadingState state) {
+               this.state = state;
+       }
+
+       @Override
+       protected Class<BooksService> getServiceClass() {
+               return BooksService.class;
+       }
+
+       @Override
+       public Call<List<BookModel>> 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 (file)
index 0000000..a525498
--- /dev/null
@@ -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<List<BookModel>, BooksService> {
+
+       @Override
+       public Call<List<BookModel>> execute(BooksService service) {
+               return service.getRecommended();
+       }
+
+       @Override
+       protected Class<BooksService> 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 (file)
index 0000000..73c7667
--- /dev/null
@@ -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<BookModel> {
+
+       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 (file)
index 0000000..4dcc724
--- /dev/null
@@ -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<BookModel, BookViewHolder> {
+
+       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 (file)
index 0000000..e029f1b
--- /dev/null
@@ -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<LibraryPresenter> implements LibraryView {
+
+       @BindView(R.id.rlReadingNowContainer)
+       View rlReadingNowContainer;
+       @BindView(R.id.rvNowReading)
+       ProgressRecyclerView<BookModel> rvNowReading;
+       @BindView(R.id.rvNewest)
+       ProgressRecyclerView<BookModel> rvNewest;
+       @BindView(R.id.rvRecommended)
+       ProgressRecyclerView<BookModel> 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<BookModel> 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<BookModel> 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<BookModel> 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<BookModel> 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 (file)
index 0000000..88f9b8b
--- /dev/null
@@ -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<LibraryView> {
+
+       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<List<BookDetailsModel>, BooksService>() {
+                       @Override
+                       public void onSuccess(List<BookDetailsModel> 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<List<BookDetailsModel>> execute(BooksService service) {
+                               return service.getPreview();
+                       }
+               }, BooksService.class);
+       }
+
+
+       private class NewestDataObserver implements DataObserver<List<BookModel>> {
+
+               @Override
+               public void onLoadStarted() {
+                       getView().setNewestProgressVisible(true);
+               }
+
+               @Override
+               public void onLoadSuccess(List<BookModel> data) {
+                       getView().setNewestProgressVisible(false);
+                       getView().setNewest(data);
+               }
+
+               @Override
+               public void onLoadFailed(Exception e) {
+                       getView().setNewestProgressVisible(false);
+                       getView().showNewestError(e);
+               }
+       }
+
+       private class RecommendedDataObserver implements DataObserver<List<BookModel>> {
+
+               @Override
+               public void onLoadStarted() {
+                       getView().setRecommendedProgressVisible(true);
+               }
+
+               @Override
+               public void onLoadSuccess(List<BookModel> data) {
+                       getView().setRecommendedProgressVisible(false);
+                       getView().setRecommended(data);
+               }
+
+               @Override
+               public void onLoadFailed(Exception e) {
+                       getView().setRecommendedProgressVisible(false);
+                       getView().showRecommendedError(e);
+               }
+       }
+
+       private class NowReadingDataObserver implements DataObserver<List<BookModel>> {
+
+               @Override
+               public void onLoadStarted() {
+                       getView().setNowReadingProgressVisible(true);
+               }
+
+               @Override
+               public void onLoadSuccess(List<BookModel> 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 (file)
index 0000000..a4955d5
--- /dev/null
@@ -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<BookModel> books);
+
+       void setNewestProgressVisible(boolean visible);
+
+       void showNewestError(Exception e);
+
+       void setRecommended(List<BookModel> books);
+
+       void setRecommendedProgressVisible(boolean visible);
+
+       void showRecommendedError(Exception e);
+
+       void setNowReadingVisibility(boolean visible);
+
+       void setNowReading(List<BookModel> 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 (file)
index 0000000..0df9ce7
--- /dev/null
@@ -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<LoginPresenter> 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 (file)
index 0000000..c0ecd41
--- /dev/null
@@ -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<LoginView> {
+
+       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<OAuthTokenModel, UserService>() {
+
+                       @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<OAuthTokenModel> 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 (file)
index 0000000..b734c4f
--- /dev/null
@@ -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 (file)
index 0000000..4917561
--- /dev/null
@@ -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<MainPresenter> 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 (file)
index 0000000..0e0c452
--- /dev/null
@@ -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<MainView> {
+
+       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<OAuthTokenModel, UserService>() {
+
+                       @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<OAuthTokenModel> 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<OAuthTokenModel, UserService>() {
+
+                       @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<OAuthTokenModel> 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<UserModel, UserService>() {
+                       @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<UserModel> execute(UserService service) {
+                               return service.getUser();
+                       }
+               }, UserService.class);
+       }
+
+       private void fetchHeader() {
+               getView().setProgressDialogVisibility(true);
+               currentCall = client.call(new RestClientCallback<List<BookDetailsModel>, BooksService>() {
+                       @Override
+                       public void onSuccess(List<BookDetailsModel> 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<List<BookDetailsModel>> execute(BooksService service) {
+                               return service.getPreview();
+                       }
+               }, BooksService.class);
+       }
+
+       private void checkPremiumStatus() {
+               checkCall = client.call(new RestClientCallback<UserModel, UserService>() {
+                       @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<UserModel> 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 (file)
index 0000000..aa764de
--- /dev/null
@@ -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 (file)
index 0000000..58f0597
--- /dev/null
@@ -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<NavigationElement, ViewHolder<NavigationElement>> {
+
+       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<NavigationElement> 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 (file)
index 0000000..fbc2399
--- /dev/null
@@ -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<NavigationElement> {
+
+       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 (file)
index 0000000..219a154
--- /dev/null
@@ -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<NavigationElement> 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 (file)
index 0000000..28e23e4
--- /dev/null
@@ -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<NavigationElement> {
+
+       @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 (file)
index 0000000..546ae15
--- /dev/null
@@ -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<NavigationElement> {
+
+       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 (file)
index 0000000..39855e0
--- /dev/null
@@ -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<NavigationElement> {
+
+       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 (file)
index 0000000..2d4a290
--- /dev/null
@@ -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 (file)
index 0000000..8e603a2
--- /dev/null
@@ -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<NewsModel, NewsListAdapter.NewsViewHolder> {
+
+       static class NewsViewHolder extends ViewHolder<NewsModel> {
+
+               @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 (file)
index 0000000..f10688c
--- /dev/null
@@ -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<NewsListPresenter> implements NewsListView {
+
+    public static NewsListFragment newInstance() {
+        return new NewsListFragment();
+    }
+
+    @BindView(R.id.rvNews)
+    ProgressRecyclerView<NewsModel> 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<NewsModel> 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 (file)
index 0000000..4b87b96
--- /dev/null
@@ -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<NewsListView> {
+
+       private class NewsListDataProvider extends DataProvider<List<NewsModel>, NewsService> {
+
+               @Override
+               protected Class<NewsService> getServiceClass() {
+                       return NewsService.class;
+               }
+
+               @Override
+               public Call<List<NewsModel>> execute(NewsService service) {
+                       return service.getNews(lastKeySlug, RestClient.PAGINATION_LIMIT);
+               }
+       }
+
+       private class NewsListDataObserver implements DataObserver<List<NewsModel>> {
+
+               @Override
+               public void onLoadStarted() {
+                       getView().setProgressVisible(true);
+               }
+
+               @Override
+               public void onLoadSuccess(List<NewsModel> 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 (file)
index 0000000..35bfc0b
--- /dev/null
@@ -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<List<NewsModel>> {
+
+       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 (file)
index 0000000..f5d4373
--- /dev/null
@@ -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 (file)
index 0000000..cc2e108
--- /dev/null
@@ -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<NewsPresenter> 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<String> 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 (file)
index 0000000..a5dff6a
--- /dev/null
@@ -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<String> galleryUrls;
+
+       private PublishSubject<Integer> pagerOnClickSubject = PublishSubject.create();
+       private View.OnClickListener pageClickListener = v -> {
+               int position = (int) v.getTag();
+               pagerOnClickSubject.onNext(position);
+       };
+
+       NewsGalleryAdapter(List<String> 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<Integer> 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 (file)
index 0000000..0b0b476
--- /dev/null
@@ -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<NewsView> {
+
+       private final NewsModel news;
+
+       NewsPresenter(NewsModel news, NewsView view) {
+               super(view);
+               this.news = news;
+
+               List<String> 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 (file)
index 0000000..d6180d5
--- /dev/null
@@ -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 (file)
index 0000000..63c4f81
--- /dev/null
@@ -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<String> 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<String> 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 (file)
index 0000000..5962e4f
--- /dev/null
@@ -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<ZoomPresenter> 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<String> 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<String> 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<String> 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 (file)
index 0000000..839a91e
--- /dev/null
@@ -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<String> photoUrls;
+       private LayoutInflater inflater;
+       private WeakReference<Context> mContext;
+
+       ZoomPhotosAdapter(List<String> 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<String, GlideDrawable>() {
+                               @Override
+                               public boolean onException(Exception e, String model, Target<GlideDrawable> 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<GlideDrawable> 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 (file)
index 0000000..77ef263
--- /dev/null
@@ -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<ZoomView> {
+
+       private final List<String> photoUrls;
+       private final int initialPosition;
+
+       ZoomPresenter(List<String> 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 (file)
index 0000000..bef12ae
--- /dev/null
@@ -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<String> 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 (file)
index 0000000..30aa6ce
--- /dev/null
@@ -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 (file)
index 0000000..ca92d2c
--- /dev/null
@@ -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<PlayerPresenter> 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 (file)
index 0000000..c409c75
--- /dev/null
@@ -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<PlayerView> {
+
+       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<MediaBrowserCompat.MediaItem> 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.
+        * <p>
+        * Here would also be where one could override
+        * {@code onQueueChanged(List<MediaSessionCompat.QueueItem> 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<MediaSessionCompat.QueueItem> 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 (file)
index 0000000..03f6a6d
--- /dev/null
@@ -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 (file)
index 0000000..12902fd
--- /dev/null
@@ -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<PlayerHeaderPresenter> 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 (file)
index 0000000..4cf7a86
--- /dev/null
@@ -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<PlayerHeaderView> {
+
+    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 (file)
index 0000000..eed238b
--- /dev/null
@@ -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 (file)
index 0000000..91b38c8
--- /dev/null
@@ -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<MediaModel, PlayerPlaylistAdapter.PlayerViewHolder> {
+
+       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<MediaModel> {
+
+               @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 (file)
index 0000000..c1333db
--- /dev/null
@@ -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<PlayerPlaylistPresenter> implements PlayerPlaylistView {
+
+       private static final String MEDIA_FILES_KEY = "MediaFilesKey";
+
+       @BindView(R.id.rvPlayerPlaylist)
+       ProgressRecyclerView<MediaModel> rvPlayerPlaylist;
+
+       private PlayerPlaylistAdapter adapter;
+
+       public static PlayerPlaylistFragment newInstance(List<MediaModel> 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<MediaModel> 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<MediaModel> 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 (file)
index 0000000..6aa2eb7
--- /dev/null
@@ -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<PlayerPlaylistView> {
+
+       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<MediaModel> media;
+       private MediaBrowserHelper mMediaBrowserHelper;
+
+       PlayerPlaylistPresenter(List<MediaModel> 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 (file)
index 0000000..3a53403
--- /dev/null
@@ -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<MediaModel> 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 (executable)
index 0000000..1fc9918
--- /dev/null
@@ -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<String, MediaMetadataCompat> music = new TreeMap<>();
+//    private static final HashMap<String, Integer> albumRes = new HashMap<>();
+    private static final HashMap<String, String> 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<MediaBrowserCompat.MediaItem> getMediaItems() {
+        List<MediaBrowserCompat.MediaItem> 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<MediaModel> 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 (executable)
index 0000000..a95f53f
--- /dev/null
@@ -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<List<MediaBrowserCompat.MediaItem>> result) {
+        result.sendResult(AudiobookLibrary.getMediaItems());
+    }
+
+    // MediaSession Callback: Transport Controls -> MediaPlayerAdapter
+    public class MediaSessionCallback extends MediaSessionCompat.Callback {
+        private final List<MediaSessionCompat.QueueItem> 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 (executable)
index 0000000..413b84b
--- /dev/null
@@ -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<? extends MediaBrowserServiceCompat> mMediaBrowserServiceClass;
+
+    private final List<Callback> 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<? extends MediaBrowserServiceCompat> 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}.
+     * <p>
+     * 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<MediaBrowserCompat.MediaItem> 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<MediaBrowserCompat.MediaItem> 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 (executable)
index 0000000..b11607b
--- /dev/null
@@ -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 (executable)
index 0000000..2bb3a57
--- /dev/null
@@ -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 (executable)
index 0000000..18dc813
--- /dev/null
@@ -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 (executable)
index 0000000..1182a11
--- /dev/null
@@ -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 (file)
index 0000000..e74879f
--- /dev/null
@@ -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<CategoryModel, BookSearchFiltersAdapter.FilterViewHolder> {
+
+       static class FilterViewHolder extends ViewHolder<CategoryModel> {
+
+               @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 (file)
index 0000000..c29522f
--- /dev/null
@@ -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<BookSearchPresenter> 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<BookModel> rvBooks;
+       @BindView(R.id.pbLoadMore)
+       ProgressBar pbLoadMore;
+       @BindView(R.id.btnReloadMore)
+       Button btnReloadMore;
+
+       private SearchView svSearch;
+
+       private BookSearchFiltersAdapter filtersAdapter;
+       private RecyclerAdapter.OnItemClickListener<CategoryModel> filtersAdapterClickListener = new RecyclerAdapter
+                       .OnItemClickListener<CategoryModel>() {
+               @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<BookModel> 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<BookModel> 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<CategoryModel> 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 (file)
index 0000000..fef67ac
--- /dev/null
@@ -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<BookSearchView> {
+
+       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<List<BookModel>> 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<CategoryModel> 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<List<BookModel>, BooksService>() {
+                       @Override
+                       public void onSuccess(List<BookModel> 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<List<BookModel>> 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 (file)
index 0000000..d2c873c
--- /dev/null
@@ -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<List<BookModel>> {
+
+       void presentBookDetails(String bookSlug);
+
+       void applyFilters(List<CategoryModel> 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 (file)
index 0000000..dec9b6d
--- /dev/null
@@ -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 (file)
index 0000000..529968c
--- /dev/null
@@ -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<CategoryModel> 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<CategoryModel> 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<CategoryModel> getSelectedCategories() {
+               List<CategoryModel> 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 (file)
index 0000000..4e25726
--- /dev/null
@@ -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<CategoryModel> filteredEpochs;
+       private List<CategoryModel> filteredGenres;
+       private List<CategoryModel> 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<CategoryModel> getFilteredEpochs() {
+               return filteredEpochs;
+       }
+
+       public void setFilteredEpochs(List<CategoryModel> filteredEpochs) {
+               this.filteredEpochs = filteredEpochs;
+       }
+
+       public List<CategoryModel> getFilteredGenres() {
+               return filteredGenres;
+       }
+
+       public void setFilteredGenres(List<CategoryModel> filteredGenres) {
+               this.filteredGenres = filteredGenres;
+       }
+
+       public List<CategoryModel> getFilteredKinds() {
+               return filteredKinds;
+       }
+
+       public void setFilteredKinds(List<CategoryModel> 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 (file)
index 0000000..0c394f9
--- /dev/null
@@ -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 (file)
index 0000000..4ae7718
--- /dev/null
@@ -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<FilterPresenter> 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<CategoryModel> data) {
+               pflEpochs.addCategories(data);
+       }
+
+       @Override
+       public void displayGenres(List<CategoryModel> data) {
+               pflGenres.addCategories(data);
+       }
+
+       @Override
+       public void displayKinds(List<CategoryModel> 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 (file)
index 0000000..c473578
--- /dev/null
@@ -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<FilterView> {
+
+       private FilterDto previousFilters;
+       private RestClient restClient;
+
+       private Call<List<CategoryModel>> epochsCall;
+       private Call<List<CategoryModel>> genresCall;
+       private Call<List<CategoryModel>> 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<List<CategoryModel>, CategoriesService>() {
+                       @Override
+                       public void onSuccess(List<CategoryModel> data) {
+                               matchFilters(previousFilters.getFilteredKinds(), data);
+                               getView().displayKinds(data);
+                       }
+
+                       @Override
+                       public void onFailure(Exception e) {
+                               getView().showKindsError();
+                       }
+
+                       @Override
+                       public void onCancel() {
+                               // nop.
+                       }
+
+                       @Override
+                       public Call<List<CategoryModel>> execute(CategoriesService service) {
+                               return service.getKinds(true);
+                       }
+               }, CategoriesService.class);
+       }
+
+       void loadGenres() {
+               genresCall = restClient.call(new RestClientCallback<List<CategoryModel>, CategoriesService>() {
+                       @Override
+                       public void onSuccess(List<CategoryModel> data) {
+                               matchFilters(previousFilters.getFilteredGenres(), data);
+                               getView().displayGenres(data);
+                       }
+
+                       @Override
+                       public void onFailure(Exception e) {
+                               getView().showGenresError();
+                       }
+
+                       @Override
+                       public void onCancel() {
+                               // nop.
+                       }
+
+                       @Override
+                       public Call<List<CategoryModel>> execute(CategoriesService service) {
+                               return service.getGenres(true);
+                       }
+               }, CategoriesService.class);
+       }
+
+       void loadEpochs() {
+               epochsCall = restClient.call(new RestClientCallback<List<CategoryModel>, CategoriesService>() {
+                       @Override
+                       public void onSuccess(List<CategoryModel> data) {
+                               matchFilters(previousFilters.getFilteredEpochs(), data);
+                               getView().displayEpochs(data);
+                       }
+
+                       @Override
+                       public void onFailure(Exception e) {
+                               getView().showEpochsError();
+                       }
+
+                       @Override
+                       public void onCancel() {
+                               // nop.
+                       }
+
+                       @Override
+                       public Call<List<CategoryModel>> execute(CategoriesService service) {
+                               return service.getEpochs(true);
+                       }
+               }, CategoriesService.class);
+       }
+
+       private void matchFilters(List<CategoryModel> previousFilters, List<CategoryModel> 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<CategoryModel> epochs, List<CategoryModel> genres, List<CategoryModel> 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 (file)
index 0000000..60ed66c
--- /dev/null
@@ -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<CategoryModel> data);
+
+       void displayGenres(List<CategoryModel> data);
+
+       void displayKinds(List<CategoryModel> 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 (file)
index 0000000..753addb
--- /dev/null
@@ -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<SettingsPresenter> 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 (file)
index 0000000..85b6f13
--- /dev/null
@@ -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<SettingsView> {
+
+       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<BookModel> 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 (file)
index 0000000..461cfa7
--- /dev/null
@@ -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 (file)
index 0000000..1088196
--- /dev/null
@@ -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 (file)
index 0000000..c8fd191
--- /dev/null
@@ -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 (file)
index 0000000..9cafd08
--- /dev/null
@@ -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 (file)
index 0000000..70e59d9
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:color="@color/colorPrimary"/>
+    <item android:color="@color/white"/>
+</selector>
\ 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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (file)
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 (file)
index 0000000..f77d3af
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+        android:color="@color/colorPrimary">
+    <item>
+        <selector>
+            <item
+                android:drawable="@drawable/nav_item_background"
+                android:state_activated="true"/>
+            <item
+                android:drawable="@drawable/nav_item_background"
+                android:state_selected="true"/>
+            <item
+                android:drawable="@drawable/nav_item_background"
+                android:state_pressed="true"/>
+
+            <item android:drawable="@color/colorPrimaryDark"/>
+
+        </selector>
+    </item>
+</ripple>
\ 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 (file)
index 0000000..3bb4cdb
--- /dev/null
@@ -0,0 +1,34 @@
+<vector xmlns:aapt="http://schemas.android.com/aapt"
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="108dp"
+        android:height="108dp"
+        android:viewportHeight="108"
+        android:viewportWidth="108">
+    <path
+        android:fillType="evenOdd"
+        android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+        android:strokeColor="#00000000"
+        android:strokeWidth="1">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="78.5885"
+                android:endY="90.9159"
+                android:startX="48.7653"
+                android:startY="61.0927"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0"/>
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0"/>
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+        android:strokeColor="#00000000"
+        android:strokeWidth="1"/>
+</vector>
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (executable)
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 (executable)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..b6105f6
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_accept_new"
+        android:tint="@color/orange_light"/>
\ 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 (file)
index 0000000..fc29851
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_accept"
+        android:tint="@color/white"/>
\ 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 (file)
index 0000000..3a1e257
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="5dp"/>
+            <solid android:color="#27AE60"/>
+        </shape>
+    </item>
+</selector>
\ 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 (file)
index 0000000..b4da8f9
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true">
+        <shape android:shape="oval">
+            <solid android:color="@color/red_dark"/>
+        </shape>
+    </item>
+    <item android:state_focused="true">
+        <shape android:shape="oval">
+            <solid android:color="@color/red_dark"/>
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="oval">
+            <solid android:color="@color/white"/>
+        </shape>
+    </item>
+</selector>
\ 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 (file)
index 0000000..acd345b
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_close_small"
+        android:tint="@color/turquoise_dark"/>
\ 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 (file)
index 0000000..dfe1965
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_close_small"
+        android:tint="@color/white"/>
\ 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 (file)
index 0000000..5225fdd
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/trash_active_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/trash_active_tint" />
+    <item android:drawable="@drawable/trash_tint" />
+</selector>
\ 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 (file)
index 0000000..15d3a6c
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:state_pressed="true">
+        <layer-list>
+            <item>
+                <shape android:shape="rectangle">
+                    <solid android:color="@color/red_dark"/>
+                    <corners android:radius="100dp"/>
+                </shape>
+            </item>
+
+            <item android:bottom="1dp" android:left="1dp" android:right="1dp" android:top="1dp">
+                <shape android:shape="rectangle">
+                    <solid android:color="@color/turquoise"/>
+                    <corners android:radius="99dp"/>
+                </shape>
+            </item>
+
+        </layer-list>
+    </item>
+
+    <item>
+        <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+            <item>
+                <shape android:shape="rectangle">
+                    <solid android:color="@color/white"/>
+                    <corners android:radius="100dp"/>
+                </shape>
+            </item>
+
+            <item android:bottom="1dp" android:left="1dp" android:right="1dp" android:top="1dp">
+                <shape android:shape="rectangle">
+                    <solid android:color="@color/turquoise_dark"/>
+                    <corners android:radius="99dp"/>
+                </shape>
+            </item>
+
+        </layer-list>
+    </item>
+
+</selector>
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 (file)
index 0000000..e02e3a0
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_filter"
+        android:tint="@color/white"/>
\ 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 (file)
index 0000000..38fbc26
--- /dev/null
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
+</vector>
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 (file)
index 0000000..3760095
--- /dev/null
@@ -0,0 +1,4 @@
+<vector android:height="24dp" android:viewportHeight="24.0"
+    android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FFFFFFFF" android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
+</vector>
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 (file)
index 0000000..32c9f8f
--- /dev/null
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+    <path
+        android:fillColor="#414141"
+        android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
+</vector>
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 (file)
index 0000000..6ddda40
--- /dev/null
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FFF"
+        android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
+</vector>
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 (file)
index 0000000..5884e55
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+    android:src="@drawable/ic_fav"
+    android:tint="@color/orange_light"/>
\ 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 (file)
index 0000000..807d592
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+    android:src="@drawable/ic_glass_mid"
+    android:tint="@color/white"/>
\ 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 (file)
index 0000000..5713f34
--- /dev/null
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+    <path
+        android:fillColor="#26A69A"
+        android:pathData="M0,0h108v108h-108z"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8"/>
+</vector>
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 (file)
index 0000000..30ef280
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_menu_star"
+        android:tint="@color/white"/>
\ 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 (file)
index 0000000..70e3f7f
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/player_chapter_next"
+        android:tint="@color/turquoise"/>
\ 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 (file)
index 0000000..b5696d3
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/player_chapter_previous"
+        android:tint="@color/turquoise"/>
\ 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 (file)
index 0000000..b430b65
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_prev"
+        android:tint="@color/white"/>
\ 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 (file)
index 0000000..a8175c3
--- /dev/null
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
+</vector>
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 (file)
index 0000000..3c977e4
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+    android:src="@drawable/ic_speaker_mid"
+    android:tint="@color/white"/>
\ 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 (file)
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 (file)
index 0000000..42a64cf
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/white"/>
+        </shape>
+    </item>
+    <item
+        android:left="2dp">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/turquoise_dark"/>
+        </shape>
+    </item>
+
+</layer-list>
\ 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 (file)
index 0000000..e2ecafd
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@drawable/nav_item_background" android:state_activated="true"/>
+    <item android:drawable="@drawable/nav_item_background" android:state_selected="true"/>
+    <item android:drawable="@drawable/nav_item_background" android:state_pressed="true"/>
+    <item android:drawable="@color/colorPrimaryDark"/>
+
+</selector>
\ 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 (file)
index 0000000..fc027a4
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/player_controls_forward"
+        android:tint="@color/turquoise"/>
\ 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 (file)
index 0000000..1860bfa
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/next_con_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/next_con_tint" />
+    <item android:drawable="@drawable/player_controls_forward" />
+</selector>
\ 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 (file)
index 0000000..ac5747c
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/ic_next_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/ic_next_tint" />
+    <item android:drawable="@drawable/player_chapter_next" />
+</selector>
\ 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 (file)
index 0000000..bdbdb6f
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <corners
+        android:radius="@dimen/book_details_corner_radius"/>
+    <solid android:color="@color/orange_light"/>
+
+</shape>
\ 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 (file)
index 0000000..55e2d7d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <corners
+        android:radius="20dp"/>
+
+    <solid android:color="@color/orange_light"/>
+
+</shape>
\ 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 (file)
index 0000000..e7f79f3
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/pause_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/pause_tint" />
+    <item android:drawable="@drawable/player_controls_pause" />
+</selector>
\ 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 (file)
index 0000000..f0eaba1
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/player_controls_pause"
+        android:tint="@color/turquoise"/>
\ 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 (file)
index 0000000..c6aa8b9
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/play_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/play_tint" />
+    <item android:drawable="@drawable/player_controls_play" />
+</selector>
\ 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 (file)
index 0000000..ac3535e
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/player_controls_play"
+        android:tint="@color/turquoise"/>
\ 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 (file)
index 0000000..ec9a15b
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/play_icon"
+        android:tint="@color/white"/>
\ 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 (file)
index 0000000..a68d60a
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/prev_con_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/prev_con_tint" />
+    <item android:drawable="@drawable/player_controls_rewind" />
+</selector>
\ 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 (file)
index 0000000..d0d0ce0
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/player_controls_rewind"
+        android:tint="@color/turquoise"/>
\ 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 (file)
index 0000000..d56f0a4
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/ic_prev_gray_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/ic_prev_gray_tint" />
+    <item android:drawable="@drawable/player_chapter_previous" />
+</selector>
\ 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 (file)
index 0000000..8b2ab2e
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="oval">
+            <solid android:color="@color/turquoise_dark"/>
+        </shape>
+    </item>
+
+    <item android:bottom="1dp" android:left="1dp" android:right="1dp" android:top="1dp">
+        <shape android:shape="oval">
+            <solid android:color="@color/white"/>
+        </shape>
+    </item>
+</layer-list>
\ 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 (file)
index 0000000..c06cda8
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_reload"
+        android:tint="@color/red_dark"/>
\ 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 (file)
index 0000000..f4a550d
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/refresh_active_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/refresh_active_tint" />
+    <item android:drawable="@drawable/refresh_tint_white" />
+</selector>
\ 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 (file)
index 0000000..f7eae49
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/refresh_active_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/refresh_active_tint" />
+    <item android:drawable="@drawable/refresh_tint" />
+</selector>
\ 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 (file)
index 0000000..d64d53e
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_reload"
+        android:tint="@color/turquoise_dark"/>
\ 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 (file)
index 0000000..2c8a621
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_reload"
+        android:tint="@color/white"/>
\ 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 (file)
index 0000000..ef0b13d
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="oval">
+
+    <solid android:color="@color/orange_overlay"/>
+</shape>
\ 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 (file)
index 0000000..7257e88
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_search"
+        android:tint="@color/white"/>
\ 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 (file)
index 0000000..6c93427
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@android:id/background"
+        android:gravity="center_vertical|fill_horizontal">
+        <shape
+            android:shape="rectangle">
+            <corners android:radius="8dp"/>
+            <size android:height="3dp"/>
+            <solid android:color="@color/app_gray"/>
+        </shape>
+    </item>
+    <item
+        android:id="@android:id/progress"
+        android:gravity="center_vertical|fill_horizontal">
+        <scale android:scaleWidth="100%">
+            <shape
+                android:shape="rectangle">
+                <corners android:radius="8dp"/>
+                <size android:height="3dp"/>
+                <solid android:color="@color/turquoise"/>
+            </shape>
+        </scale>
+    </item>
+</layer-list>
\ 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 (file)
index 0000000..5ff81b8
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/seekbar_thumb"
+        android:tint="@color/turquoise"/>
\ 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 (file)
index 0000000..57d641f
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:state_pressed="true">
+        <shape android:shape="rectangle">
+            <corners android:radius="20dp"/>
+            <solid android:color="@color/white"/>
+        </shape>
+    </item>
+
+    <item>
+        <layer-list>
+            <item>
+                <shape android:shape="rectangle">
+                    <corners android:radius="20dp"/>
+                    <solid android:color="@color/white"/>
+                </shape>
+            </item>
+            <item
+                android:bottom="@dimen/rounded_buttons_border_width"
+                android:left="@dimen/rounded_buttons_border_width"
+                android:right="@dimen/rounded_buttons_border_width"
+                android:top="@dimen/rounded_buttons_border_width">
+                <shape android:shape="rectangle">
+                    <corners android:radius="20dp"/>
+                    <solid android:color="@color/colorPrimary"/>
+                </shape>
+            </item>
+        </layer-list>
+    </item>
+
+</selector>
\ 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 (file)
index 0000000..c2b3dfe
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/toggle_tint" />
+    <item android:state_focused="true" android:drawable="@drawable/toggle_tint" />
+    <item android:drawable="@drawable/ic_toggle" />
+</selector>
\ 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 (file)
index 0000000..f21e14f
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_toggle"
+        android:tint="@color/turquoise"/>
\ 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 (file)
index 0000000..ad1499d
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_trash"
+        android:tint="@color/red_dark"/>
\ 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 (file)
index 0000000..ff47951
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_trash"
+        android:tint="@color/turquoise_dark"/>
\ 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 (file)
index 0000000..30ef280
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_menu_star"
+        android:tint="@color/white"/>
\ 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 (file)
index 0000000..2eb057a
--- /dev/null
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/colorPrimary">
+
+    <ImageButton
+        android:id="@+id/ibBack"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="16dp"
+        android:background="?attr/selectableItemBackgroundBorderless"
+        android:src="?attr/homeAsUpIndicator"
+        android:tint="@color/white"
+        tools:ignore="ContentDescription"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:orientation="vertical"
+        android:paddingEnd="30dp"
+        android:paddingStart="30dp">
+
+        <TextView
+            style="@style/PlayerPlaylistText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="30dp"
+            android:gravity="center"
+            android:text="@string/login_title"
+            android:textSize="22dp"
+            tools:ignore="SpUsage"/>
+
+        <TextView
+            style="@style/LoginBenefitsText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/login_benefits"/>
+
+        <TextView
+            style="@style/LoginBenefitsText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/login_benefits_2"/>
+
+        <TextView
+            style="@style/LoginBenefitsText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/login_benefits_3"/>
+
+        <Button
+            android:id="@+id/btnLogin"
+            style="@style/RoundedButton.WhiteBorder"
+            android:layout_width="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginTop="40dp"
+            android:paddingEnd="40dp"
+            android:paddingStart="40dp"
+            android:text="@string/menu_login"/>
+    </LinearLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout-v21/fragment_player_header.xml b/Android/app/src/main/res/layout-v21/fragment_player_header.xml
new file mode 100644 (file)
index 0000000..1207ad7
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/colorPrimary"
+    tools:ignore="SpUsage">
+
+    <ImageView
+        android:id="@+id/ivCoverBackground"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:contentDescription="@string/app_name"
+        android:scaleType="centerCrop"/>
+
+    <View
+        android:id="@+id/vCoverOverlay"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <ImageButton
+        android:id="@+id/ibBack"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="16dp"
+        android:background="?attr/selectableItemBackgroundBorderless"
+        android:src="?attr/homeAsUpIndicator"
+        android:tint="@color/white"
+        tools:ignore="ContentDescription"/>
+
+    <TextView
+        android:id="@+id/tvAuthor"
+        style="@style/BookHeaderTextView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/ibBack"
+        android:layout_marginEnd="24dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginTop="20dp"
+        android:text="Adam Mickiewicz"
+        android:textSize="18dp"/>
+
+    <TextView
+        android:id="@+id/tvBookTitle"
+        style="@style/BookHeaderTextView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/tvAuthor"
+        android:layout_marginEnd="24dp"
+        android:layout_marginStart="24dp"
+        android:text="Dziady"
+        android:textSize="26dp"/>
+
+    <ImageView
+        android:id="@+id/ivCover"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_below="@id/tvBookTitle"
+        android:layout_marginTop="20dp"
+        android:adjustViewBounds="true"
+        android:src="@drawable/list_nocover"
+        tools:ignore="ContentDescription"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/activity_blank.xml b/Android/app/src/main/res/layout/activity_blank.xml
new file mode 100644 (file)
index 0000000..de0ba15
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    android:id="@+id/flContainer"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
diff --git a/Android/app/src/main/res/layout/activity_login.xml b/Android/app/src/main/res/layout/activity_login.xml
new file mode 100644 (file)
index 0000000..ead6bf6
--- /dev/null
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/colorPrimary">
+
+    <ImageButton
+        android:id="@+id/ibBack"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="16dp"
+        android:background="?attr/selectableItemBackgroundBorderless"
+        android:src="@drawable/ic_prev_tint"
+        android:tint="@color/white"
+        tools:ignore="ContentDescription"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:orientation="vertical"
+        android:paddingEnd="30dp"
+        android:paddingStart="30dp">
+
+        <TextView
+            style="@style/PlayerPlaylistText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="30dp"
+            android:gravity="center"
+            android:text="@string/login_title"
+            android:textSize="22dp"
+            tools:ignore="SpUsage"/>
+
+        <TextView
+            style="@style/LoginBenefitsText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/login_benefits"/>
+
+        <TextView
+            style="@style/LoginBenefitsText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/login_benefits_2"/>
+
+        <TextView
+            style="@style/LoginBenefitsText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/login_benefits_3"/>
+
+        <Button
+            android:id="@+id/btnLogin"
+            style="@style/RoundedButton.WhiteBorder"
+            android:layout_width="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginTop="40dp"
+            android:paddingEnd="40dp"
+            android:paddingStart="40dp"
+            android:text="@string/menu_login"/>
+    </LinearLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/activity_main.xml b/Android/app/src/main/res/layout/activity_main.xml
new file mode 100644 (file)
index 0000000..142a5b9
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.DrawerLayout
+    android:id="@+id/drawer_layout"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:ignore="SpUsage">
+
+    <!-- The main content view -->
+    <FrameLayout
+        android:id="@+id/content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <!-- The navigation drawer -->
+    <RelativeLayout
+        android:layout_width="300dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"
+        android:background="@color/turquoise_dark">
+
+        <include layout="@layout/navigation_footer"/>
+
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/rvNavigation"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_above="@id/llProfileContainer"/>
+    </RelativeLayout>
+
+</android.support.v4.widget.DrawerLayout>
diff --git a/Android/app/src/main/res/layout/activity_splash.xml b/Android/app/src/main/res/layout/activity_splash.xml
new file mode 100644 (file)
index 0000000..146ebca
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout android:id="@+id/rlMainView"
+                xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/white">
+
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:src="@drawable/splash"
+        tools:ignore="ContentDescription"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/book_item.xml b/Android/app/src/main/res/layout/book_item.xml
new file mode 100644 (file)
index 0000000..9f16109
--- /dev/null
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="145dp"
+    android:layout_height="200dp"
+    android:layout_marginEnd="10dp"
+    android:layout_marginStart="10dp"
+    app:cardBackgroundColor="@color/white"
+    app:cardCornerRadius="8dp">
+
+    <android.support.constraint.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <ImageView
+            android:id="@+id/ivBookCover"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scaleType="fitCenter"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"/>
+
+
+        <LinearLayout
+            android:id="@+id/llBookContent"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/orange_overlay"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:padding="10dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent">
+
+
+            <TextView
+                android:id="@+id/tvBookAuthor"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:text="Maria Konopnicka"
+                android:textColor="@color/white"
+                android:textSize="13dp"/>
+
+            <TextView
+                android:id="@+id/tvBookTitle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ellipsize="end"
+                android:gravity="center"
+                android:maxLines="4"
+                android:text="Poezje dla dzieci do lat 7, część I"
+                android:textColor="@color/white"
+                android:textSize="14dp"
+                android:textStyle="bold"/>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="5dp"
+            android:orientation="horizontal"
+            app:layout_constraintBottom_toTopOf="@id/llBookContent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginEnd="5dp"
+                android:background="@drawable/round_background_overlay"
+                android:padding="4dp"
+                android:src="@drawable/ic_glass_small"
+                android:tint="@color/white"/>
+
+            <ImageView
+                android:id="@+id/ivAudioBook"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@drawable/round_background_overlay"
+                android:padding="4dp"
+                android:src="@drawable/ic_speaker_small"
+                android:tint="@color/white"/>
+
+        </LinearLayout>
+
+    </android.support.constraint.ConstraintLayout>
+</android.support.v7.widget.CardView>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/checkbox.xml b/Android/app/src/main/res/layout/checkbox.xml
new file mode 100644 (file)
index 0000000..7281ca0
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.AppCompatCheckBox
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:textAllCaps="true"
+    android:textColor="@color/white"
+    android:textSize="13dp"
+    app:buttonTint="@color/white"
+    tools:ignore="SpUsage"/>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/filter_item.xml b/Android/app/src/main/res/layout/filter_item.xml
new file mode 100644 (file)
index 0000000..4240bf8
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="wrap_content"
+                android:layout_height="@dimen/book_search_filters_height"
+                android:paddingLeft="5dp"
+                android:paddingRight="5dp">
+
+    <TextView
+        android:id="@+id/tvFilterName"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:background="@drawable/filter_background"
+        android:drawableEnd="@drawable/close_small_tint"
+        android:drawablePadding="10dp"
+        android:paddingBottom="5dp"
+        android:paddingLeft="15dp"
+        android:paddingRight="15dp"
+        android:paddingTop="5dp"
+        android:textAllCaps="true"
+        android:textColor="@color/white"
+        android:textSize="12dp"
+        tools:ignore="SpUsage"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_about.xml b/Android/app/src/main/res/layout/fragment_about.xml
new file mode 100644 (file)
index 0000000..199524d
--- /dev/null
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:tools="http://schemas.android.com/tools"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            tools:ignore="SpUsage">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:padding="30dp">
+
+        <Button
+            android:id="@+id/btnBecomeAFriend"
+            style="@style/RoundedButton.Orange"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginBottom="20dp"
+            android:text="@string/become_a_friend"/>
+
+        <ImageView
+            android:layout_width="200dp"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_marginBottom="20dp"
+            android:adjustViewBounds="true"
+            android:src="@drawable/logo_wl_light"/>
+
+        <TextView
+            android:id="@+id/tvAbout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/about_text"
+            android:textColor="@color/gray_dark"
+            android:textSize="14dp"/>
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:text="@string/about_text_fundation"
+            android:textColor="@color/gray_dark"
+            android:textSize="14dp"/>
+
+        <ImageView
+            android:layout_width="200dp"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:adjustViewBounds="true"
+            android:src="@drawable/logo_fnp"/>
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/about_text_mkdn"
+            android:textColor="@color/gray_dark"
+            android:textSize="14dp"/>
+
+        <ImageView
+            android:layout_width="170dp"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:adjustViewBounds="true"
+            android:src="@drawable/logo_mkidn"/>
+    </LinearLayout>
+
+</ScrollView>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_book.xml b/Android/app/src/main/res/layout/fragment_book.xml
new file mode 100644 (file)
index 0000000..5fa675e
--- /dev/null
@@ -0,0 +1,287 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+    <android.support.design.widget.CoordinatorLayout
+        android:id="@+id/clMainView"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <android.support.design.widget.AppBarLayout
+            android:id="@+id/app_bar_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
+
+            <android.support.design.widget.CollapsingToolbarLayout
+                android:id="@+id/ctlCollapse"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/book_header_height"
+                app:contentScrim="?attr/colorPrimary"
+                app:expandedTitleTextAppearance="@android:color/transparent"
+                app:layout_scrollFlags="snap">
+
+                <include
+                    layout="@layout/fragment_book_header"
+                    app:layout_collapseMode="parallax"/>
+
+                <android.support.v7.widget.Toolbar
+                    android:id="@+id/bookToolbar"
+                    android:layout_width="match_parent"
+                    android:layout_height="?attr/actionBarSize"
+                    android:minHeight="?attr/actionBarSize"
+                    app:layout_collapseMode="pin"
+                    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
+
+            </android.support.design.widget.CollapsingToolbarLayout>
+
+        </android.support.design.widget.AppBarLayout>
+
+        <android.support.v4.widget.NestedScrollView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="fill_vertical"
+            android:fillViewport="true"
+            app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+            <com.facebook.shimmer.ShimmerFrameLayout
+                android:id="@+id/shimmerContentContainer"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:animateLayoutChanges="true"
+                    android:orientation="vertical"
+                    tools:ignore="SpUsage">
+
+                    <LinearLayout
+                        android:id="@+id/llBookContent"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:orientation="vertical"
+                        android:paddingEnd="@dimen/book_details_padding"
+                        android:paddingStart="@dimen/book_details_padding">
+
+                        <TextView
+                            style="@style/BookCategoriesTitleText"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="20dp"
+                            android:text="@string/book_epoch"/>
+
+                        <TextView
+                            android:id="@+id/tvBookEpoch"
+                            style="@style/ListTitleText.Turquoise"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:background="@color/placeholder_gray"
+                            android:textSize="18dp"/>
+
+                        <TextView
+                            style="@style/BookCategoriesTitleText"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="10dp"
+                            android:text="@string/book_kind"/>
+
+                        <TextView
+                            android:id="@+id/tvBookKind"
+                            style="@style/ListTitleText.Turquoise"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:background="@color/placeholder_gray"
+                            android:textSize="18dp"/>
+
+                        <TextView
+                            style="@style/BookCategoriesTitleText"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="10dp"
+                            android:text="@string/book_genre"/>
+
+                        <TextView
+                            android:id="@+id/tvBookGenre"
+                            style="@style/ListTitleText.Turquoise"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:background="@color/placeholder_gray"
+                            android:textSize="18dp"/>
+
+                        <View
+                            android:layout_width="match_parent"
+                            android:layout_height="1dp"
+                            android:layout_marginBottom="10dp"
+                            android:layout_marginTop="10dp"
+                            android:background="@color/gray_very_dark"/>
+
+                        <org.sufficientlysecure.htmltextview.HtmlTextView
+                            android:id="@+id/tvQuotationText"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:background="@color/placeholder_gray"
+                            android:minLines="8"
+                            android:textSize="16dp"
+                            android:textStyle="italic"/>
+
+                        <TextView
+                            android:id="@+id/tvQuotationAuthor"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="20dp"
+                            android:background="@color/placeholder_gray"
+                            android:gravity="end"
+                            android:textSize="12dp"/>
+
+                        <View
+                            android:id="@+id/vSecondDivider"
+                            android:layout_width="match_parent"
+                            android:layout_height="1dp"
+                            android:layout_marginBottom="10dp"
+                            android:layout_marginTop="10dp"
+                            android:background="@color/gray_very_dark"/>
+
+                    </LinearLayout>
+
+                    <RelativeLayout
+                        android:id="@+id/rlEbookButtonsContainer"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_marginBottom="10dp"
+                        android:layout_marginTop="10dp"
+                        android:visibility="gone">
+
+                        <ImageButton
+                            android:id="@+id/ibDeleteEbook"
+                            android:layout_width="@dimen/book_details_button_margin"
+                            android:layout_height="@dimen/book_details_button_margin"
+                            android:layout_alignParentEnd="true"
+                            android:layout_centerVertical="true"
+                            android:background="@null"
+                            android:src="@drawable/delete_icon_selector"
+                            android:visibility="invisible"
+                            tools:ignore="ContentDescription"/>
+
+                        <com.moiseum.wolnelektury.view.book.components.ProgressDownloadButton
+                            android:id="@+id/btnEbook"
+                            android:layout_width="match_parent"
+                            android:layout_height="45dp"
+                            android:layout_centerVertical="true"
+                            android:layout_marginLeft="@dimen/book_details_button_margin"
+                            android:layout_toStartOf="@id/ibDeleteEbook"
+                            android:paddingEnd="@dimen/book_button_padding"
+                            android:paddingStart="@dimen/book_button_padding"
+                            app:border_size="1dp"
+                            app:corner_radius="@dimen/book_details_corner_radius"
+                            app:drawable="@drawable/ic_glass_mid"
+                            app:text_color="@color/orange"
+                            app:text_downloaded="@string/download_ebook_read"
+                            app:text_initial="@string/download_ebook"
+                            app:text_inverted_color="@color/white"
+                            app:text_size="@dimen/book_button_text_size"
+                            tools:ignore="RtlHardcoded"/>
+
+                    </RelativeLayout>
+
+                    <RelativeLayout
+                        android:id="@+id/rlAudioButtonsContainer"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:visibility="gone">
+
+                        <ImageButton
+                            android:id="@+id/ibDeleteAudiobook"
+                            android:layout_width="@dimen/book_details_button_margin"
+                            android:layout_height="@dimen/book_details_button_margin"
+                            android:layout_alignParentEnd="true"
+                            android:layout_centerVertical="true"
+                            android:background="@null"
+                            android:src="@drawable/delete_icon_selector"
+                            android:visibility="invisible"
+                            tools:ignore="ContentDescription"/>
+
+                        <com.moiseum.wolnelektury.view.book.components.ProgressDownloadButton
+                            android:id="@+id/btnAudiobook"
+                            android:layout_width="match_parent"
+                            android:layout_height="45dp"
+                            android:layout_centerVertical="true"
+                            android:layout_marginLeft="@dimen/book_details_button_margin"
+                            android:layout_toStartOf="@id/ibDeleteAudiobook"
+                            android:paddingEnd="@dimen/book_button_padding"
+                            android:paddingStart="@dimen/book_button_padding"
+                            app:border_size="1dp"
+                            app:corner_radius="@dimen/book_details_corner_radius"
+                            app:drawable="@drawable/ic_speaker_mid"
+                            app:text_color="@color/audiobook_gray"
+                            app:text_downloaded="@string/download_audiobook_read"
+                            app:text_initial="@string/download_audiobook"
+                            app:text_inverted_color="@color/white"
+                            app:text_size="@dimen/book_button_text_size"
+                            tools:ignore="RtlHardcoded"/>
+
+                    </RelativeLayout>
+
+                    <View
+                        android:layout_width="match_parent"
+                        android:layout_height="30dp" />
+
+                    <!--<Button-->
+                    <!--android:id="@+id/btnSupportUs"-->
+                    <!--style="@style/OrangeDetailsButton"-->
+                    <!--android:layout_width="match_parent"-->
+                    <!--android:layout_height="45dp"-->
+                    <!--android:layout_marginLeft="@dimen/book_details_button_margin"-->
+                    <!--android:layout_marginRight="@dimen/book_details_button_margin"-->
+                    <!--android:layout_marginTop="10dp"-->
+                    <!--android:drawableEnd="@drawable/white_star"-->
+                    <!--android:text="@string/support_us"/>-->
+
+                </LinearLayout>
+
+            </com.facebook.shimmer.ShimmerFrameLayout>
+
+        </android.support.v4.widget.NestedScrollView>
+
+        <android.support.design.widget.FloatingActionButton
+            android:id="@+id/fabShare"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="15dp"
+            android:src="@drawable/ic_share"
+            android:tint="@color/orange"
+            android:visibility="invisible"
+            app:backgroundTint="@color/white"
+            app:elevation="6dp"
+            app:layout_anchor="@id/app_bar_layout"
+            app:layout_anchorGravity="bottom|end"
+            app:pressedTranslationZ="8dp"
+            app:useCompatPadding="true"/>
+
+        <android.support.design.widget.FloatingActionButton
+            android:id="@+id/fabFavourite"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center|start"
+            android:src="@drawable/ic_fav"
+            android:tint="@color/orange"
+            android:visibility="invisible"
+            app:backgroundTint="@color/white"
+            app:elevation="6dp"
+            app:layout_anchor="@id/fabShare"
+            app:layout_anchorGravity="center|start"
+            app:pressedTranslationZ="8dp"/>
+
+    </android.support.design.widget.CoordinatorLayout>
+
+    <include
+        android:id="@+id/clPremium"
+        layout="@layout/prapremiere_info"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_book_details.xml b/Android/app/src/main/res/layout/fragment_book_details.xml
new file mode 100644 (file)
index 0000000..fbeae43
--- /dev/null
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:tools="http://schemas.android.com/tools"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:orientation="horizontal">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="vertical">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/ic_glass_big"
+            android:tint="@color/gray_very_dark"
+            tools:ignore="ContentDescription"/>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/pages"/>
+
+        <TextView
+            android:id="@+id/tvBookPages"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+    </LinearLayout>
+
+    <View
+        android:layout_width="1dp"
+        android:layout_height="match_parent"
+        android:layout_marginBottom="15dp"
+        android:layout_marginTop="15dp"
+        android:background="@color/gray_very_dark"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:text="@string/book_epoch"/>
+
+        <TextView
+            android:id="@+id/tvBookEpoch"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:text="@string/book_kind"/>
+
+        <TextView
+            android:id="@+id/tvBookKind"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:text="@string/book_genre"/>
+
+        <TextView
+            android:id="@+id/tvBookGenre"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_book_header.xml b/Android/app/src/main/res/layout/fragment_book_header.xml
new file mode 100644 (file)
index 0000000..6b2c44b
--- /dev/null
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/book_header_height"
+                android:animateLayoutChanges="true">
+
+    <ImageView
+        android:id="@+id/ivCoverBackground"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/book_header_height"
+        android:contentDescription="@string/app_name"
+        android:scaleType="centerCrop"/>
+
+    <View
+        android:id="@+id/vCoverOverlay"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/book_header_height"/>
+
+    <ImageView
+        android:id="@+id/ivCover"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentEnd="true"
+        android:layout_marginRight="15dp"
+        android:layout_marginTop="@dimen/book_header_top"
+        android:adjustViewBounds="true"
+        android:src="@drawable/list_nocover"
+        tools:ignore="ContentDescription,RtlHardcoded"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_marginTop="@dimen/book_header_top"
+        android:layout_toStartOf="@id/ivCover"
+        android:orientation="vertical"
+        android:paddingLeft="20dp"
+        android:paddingRight="20dp">
+
+        <TextView
+            android:id="@+id/tvBookAuthor"
+            style="@style/BookHeaderTextView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLines="2"/>
+
+        <TextView
+            android:id="@+id/tvBookTitle"
+            style="@style/BookHeaderTextView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLines="2"
+            android:textSize="28dp"
+            tools:ignore="SpUsage"/>
+    </LinearLayout>
+
+    <RelativeLayout
+        android:id="@+id/rlHeaderLoadingContainer"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="?attr/colorPrimary">
+
+        <ProgressBar
+            android:id="@+id/pbHeaderLoading"
+            style="@style/Base.Widget.AppCompat.ProgressBar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:theme="@style/CircularProgress"/>
+
+        <ImageButton
+            android:id="@+id/ibRetry"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:background="@null"
+            android:src="@drawable/refresh_icon_light_selector"
+            android:visibility="gone"
+            tools:ignore="ContentDescription"/>
+    </RelativeLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_books_list.xml b/Android/app/src/main/res/layout/fragment_books_list.xml
new file mode 100644 (file)
index 0000000..676f7dc
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:animateLayoutChanges="true">
+
+    <com.moiseum.wolnelektury.components.ProgressRecyclerView
+        android:id="@+id/rvBooksList"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <ProgressBar
+        android:id="@+id/pbLoadMore"
+        style="@style/Base.Widget.AppCompat.ProgressBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="10dp"
+        android:visibility="gone"/>
+
+    <Button
+        android:id="@+id/btnReloadMore"
+        style="@style/RoundedButton.WhiteBorder"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="10dp"
+        android:text="@string/load_again"
+        android:visibility="gone"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_filter.xml b/Android/app/src/main/res/layout/fragment_filter.xml
new file mode 100644 (file)
index 0000000..df61da2
--- /dev/null
@@ -0,0 +1,138 @@
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:app="http://schemas.android.com/apk/res-auto"
+            xmlns:tools="http://schemas.android.com/tools"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@color/turquoise_dark"
+            android:fillViewport="true">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:animateLayoutChanges="true"
+        android:orientation="vertical"
+        android:paddingBottom="20dp"
+        android:paddingLeft="@dimen/filters_side_padding"
+        android:paddingRight="@dimen/filters_side_padding"
+        android:paddingTop="20dp">
+
+        <android.support.v7.widget.SwitchCompat
+            android:id="@+id/swLecturesOnly"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="10dp"
+            android:text="@string/only_lecture"
+            android:textAllCaps="true"
+            android:textColor="@color/white"
+            android:textSize="14dp"
+            app:theme="@style/DarkBackgroundSwitch"
+            tools:ignore="RtlHardcoded,SpUsage"/>
+
+        <android.support.v7.widget.SwitchCompat
+            android:id="@+id/swHasAudiobook"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="10dp"
+            android:layout_marginTop="10dp"
+            android:text="@string/has_audiobook"
+            android:textAllCaps="true"
+            android:textColor="@color/white"
+            android:textSize="14dp"
+            app:theme="@style/DarkBackgroundSwitch"
+            tools:ignore="RtlHardcoded,SpUsage"/>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/filters_header_spacing"
+            android:layout_marginTop="@dimen/filters_header_spacing">
+
+            <TextView
+                android:id="@+id/tvEpochs"
+                style="@style/SeparatorText.White"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:layout_margin="10dp"
+                android:text="@string/filter_epochs"/>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_centerVertical="true"
+                android:layout_margin="@dimen/separator_padding"
+                android:layout_toEndOf="@id/tvEpochs"
+                android:background="@color/white"/>
+
+        </RelativeLayout>
+
+        <com.moiseum.wolnelektury.view.search.components.FiltersProgressFlowLayout
+            android:id="@+id/pflEpochs"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/filters_header_spacing"
+            android:layout_marginTop="@dimen/filters_header_spacing">
+
+            <TextView
+                android:id="@+id/tvKinds"
+                style="@style/SeparatorText.White"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:layout_margin="10dp"
+                android:text="@string/filter_kinds"/>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_centerVertical="true"
+                android:layout_margin="@dimen/separator_padding"
+                android:layout_toEndOf="@id/tvKinds"
+                android:background="@color/white"/>
+
+        </RelativeLayout>
+
+        <com.moiseum.wolnelektury.view.search.components.FiltersProgressFlowLayout
+            android:id="@+id/pflKinds"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/filters_header_spacing"
+            android:layout_marginTop="@dimen/filters_header_spacing">
+
+            <TextView
+                android:id="@+id/tvGenres"
+                style="@style/SeparatorText.White"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:layout_margin="10dp"
+                android:text="@string/filter_genres"/>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_centerVertical="true"
+                android:layout_margin="@dimen/separator_padding"
+                android:layout_toEndOf="@id/tvGenres"
+                android:background="@color/white"/>
+
+        </RelativeLayout>
+
+        <com.moiseum.wolnelektury.view.search.components.FiltersProgressFlowLayout
+            android:id="@+id/pflGenres"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+
+    </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_library.xml b/Android/app/src/main/res/layout/fragment_library.xml
new file mode 100644 (file)
index 0000000..e3481d9
--- /dev/null
@@ -0,0 +1,214 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:tools="http://schemas.android.com/tools"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:fillViewport="true">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:paddingBottom="20dp">
+
+        <RelativeLayout
+            android:id="@+id/rlBecomeAFriend"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:background="@color/splash_background"
+            android:padding="10dp">
+
+            <Button
+                android:id="@+id/btnBecomeAFriend"
+                style="@style/RoundedButton.Orange"
+                android:layout_width="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:drawableEnd="@drawable/ic_arrow_right_white_24dp"
+                android:text="@string/become_a_friend"
+                android:textSize="10dp"
+                tools:ignore="SpUsage"/>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_centerVertical="true"
+                android:layout_margin="@dimen/separator_padding"
+                android:layout_toStartOf="@id/btnBecomeAFriend"
+                android:background="@color/orange_light"/>
+
+        </RelativeLayout>
+
+        <View
+            android:id="@+id/vBecomeAFriendSeparator"
+            android:layout_width="match_parent"
+            android:layout_height="10dp"
+            android:background="@color/splash_background"/>
+
+        <include
+            android:id="@+id/libraryHeader"
+            layout="@layout/library_header"/>
+
+        <RelativeLayout
+            android:id="@+id/rlReadingNowContainer"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:visibility="gone">
+
+            <TextView
+                android:id="@+id/tvNowReading"
+                style="@style/SeparatorText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:layout_margin="10dp"
+                android:text="@string/library_now_reading"/>
+
+            <Button
+                android:id="@+id/btnNowReadingSeeAll"
+                style="@style/FlatButton.Separator"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:layout_centerVertical="true"
+                android:text="@string/see_all"/>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_centerVertical="true"
+                android:layout_margin="@dimen/separator_padding"
+                android:layout_toEndOf="@id/tvNowReading"
+                android:layout_toStartOf="@id/btnNowReadingSeeAll"
+                android:background="@color/gray_dark"/>
+
+        </RelativeLayout>
+
+        <com.moiseum.wolnelektury.components.ProgressRecyclerView
+            android:id="@+id/rvNowReading"
+            android:layout_width="match_parent"
+            android:layout_height="200dp"
+            android:visibility="gone"/>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <TextView
+                android:id="@+id/tvNewest"
+                style="@style/SeparatorText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:layout_margin="10dp"
+                android:text="@string/library_newest"/>
+
+            <Button
+                android:id="@+id/btnNewestSeeAll"
+                style="@style/FlatButton.Separator"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:layout_centerVertical="true"
+                android:text="@string/see_all"/>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_centerVertical="true"
+                android:layout_margin="@dimen/separator_padding"
+                android:layout_toEndOf="@id/tvNewest"
+                android:layout_toStartOf="@id/btnNewestSeeAll"
+                android:background="@color/gray_dark"/>
+
+        </RelativeLayout>
+
+        <com.moiseum.wolnelektury.components.ProgressRecyclerView
+            android:id="@+id/rvNewest"
+            android:layout_width="match_parent"
+            android:layout_height="200dp"/>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <TextView
+                android:id="@+id/tvRecommended"
+                style="@style/SeparatorText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:layout_margin="10dp"
+                android:text="@string/library_recommended"/>
+
+            <Button
+                android:id="@+id/btnRecommendedSeeAll"
+                style="@style/FlatButton.Separator"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:layout_centerVertical="true"
+                android:text="@string/see_all"/>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_centerVertical="true"
+                android:layout_margin="@dimen/separator_padding"
+                android:layout_toEndOf="@id/tvRecommended"
+                android:layout_toStartOf="@id/btnRecommendedSeeAll"
+                android:background="@color/gray_dark"/>
+
+        </RelativeLayout>
+
+        <com.moiseum.wolnelektury.components.ProgressRecyclerView
+            android:id="@+id/rvRecommended"
+            android:layout_width="match_parent"
+            android:layout_height="200dp"/>
+
+        <!--<RelativeLayout-->
+        <!--android:layout_width="match_parent"-->
+        <!--android:layout_height="wrap_content">-->
+
+        <!--<TextView-->
+        <!--android:id="@+id/tvMyCollection"-->
+        <!--style="@style/SeparatorText"-->
+        <!--android:layout_width="wrap_content"-->
+        <!--android:layout_height="wrap_content"-->
+        <!--android:layout_alignParentStart="true"-->
+        <!--android:layout_centerVertical="true"-->
+        <!--android:layout_margin="10dp"-->
+        <!--android:text="@string/library_my_collection"/>-->
+
+        <!--<Button-->
+        <!--android:id="@+id/btnMyCollectionSeeAll"-->
+        <!--style="@style/FlatButton.Separator"-->
+        <!--android:layout_width="wrap_content"-->
+        <!--android:layout_height="wrap_content"-->
+        <!--android:layout_alignParentEnd="true"-->
+        <!--android:layout_centerVertical="true"-->
+        <!--android:text="@string/see_all"/>-->
+
+        <!--<View-->
+        <!--android:layout_width="match_parent"-->
+        <!--android:layout_height="1px"-->
+        <!--android:layout_centerVertical="true"-->
+        <!--android:layout_margin="@dimen/separator_padding"-->
+        <!--android:layout_toEndOf="@id/tvMyCollection"-->
+        <!--android:layout_toStartOf="@id/btnMyCollectionSeeAll"-->
+        <!--android:background="@color/gray_dark"/>-->
+
+        <!--</RelativeLayout>-->
+
+        <!--<com.moiseum.wolnelektury.components.ProgressRecyclerView-->
+        <!--android:id="@+id/rvMyCollection"-->
+        <!--android:layout_width="match_parent"-->
+        <!--android:layout_height="200dp"/>-->
+
+
+    </LinearLayout>
+
+</ScrollView>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_news.xml b/Android/app/src/main/res/layout/fragment_news.xml
new file mode 100644 (file)
index 0000000..c7912c6
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:animateLayoutChanges="true">
+
+    <com.moiseum.wolnelektury.components.ProgressRecyclerView
+        android:id="@+id/rvNews"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <ProgressBar
+        android:id="@+id/pbLoadMore"
+        style="@style/Base.Widget.AppCompat.ProgressBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="10dp"
+        android:visibility="gone"/>
+
+    <Button
+        android:id="@+id/btnReloadMore"
+        style="@style/RoundedButton.WhiteBorder"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="10dp"
+        android:text="@string/load_again"
+        android:visibility="gone"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_player.xml b/Android/app/src/main/res/layout/fragment_player.xml
new file mode 100644 (file)
index 0000000..4a4de90
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <include
+        android:id="@+id/clControlsContainer"
+        layout="@layout/fragment_player_controls"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"/>
+
+    <FrameLayout
+        android:id="@+id/flPlayerFragmentContainer"
+        android:layout_above="@id/clControlsContainer"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_player_controls.xml b/Android/app/src/main/res/layout/fragment_player_controls.xml
new file mode 100644 (file)
index 0000000..f5b9fb0
--- /dev/null
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    tools:ignore="HardcodedText,SpUsage,ContentDescription, RtlSymmetry">
+
+    <ImageButton
+        android:id="@+id/ibToggleList"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@null"
+        android:paddingBottom="10dp"
+        android:paddingEnd="20dp"
+        android:paddingStart="20dp"
+        android:paddingTop="10dp"
+        android:src="@drawable/toggle_selector"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <LinearLayout
+        android:id="@+id/llArtistContainer"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:orientation="vertical"
+        app:layout_constraintTop_toBottomOf="@id/ibToggleList">
+
+        <TextView
+            android:id="@+id/tvArtist"
+            style="@style/BookHeaderDarkTextView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:text="Rozdział I"
+            android:textAllCaps="true"
+            android:textSize="16dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/llPlayerHeader"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        android:paddingEnd="20dp"
+        android:paddingStart="20dp"
+        app:layout_constraintTop_toBottomOf="@id/llArtistContainer">
+
+        <ImageButton
+            android:id="@+id/ibPrevious"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="0"
+            android:background="@null"
+            android:paddingBottom="@dimen/player_button_padding"
+            android:paddingEnd="10dp"
+            android:paddingTop="@dimen/player_button_padding"
+            android:src="@drawable/prev_selector"/>
+
+        <TextView
+            android:id="@+id/tvChapterTitle"
+            style="@style/BookHeaderDarkTextView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:ellipsize="end"
+            android:gravity="center"
+            android:maxLines="3"
+            android:text="Dziady. Poema Upiór"
+            android:textSize="22dp"/>
+
+        <ImageButton
+            android:id="@+id/ibNext"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_weight="0"
+            android:background="@null"
+            android:paddingBottom="@dimen/player_button_padding"
+            android:paddingStart="10dp"
+            android:paddingTop="@dimen/player_button_padding"
+            android:src="@drawable/next_selector"/>
+
+    </LinearLayout>
+
+    <View
+        android:id="@+id/vPlayerSeparator"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_marginEnd="30dp"
+        android:layout_marginStart="30dp"
+        android:layout_marginTop="15dp"
+        android:background="@color/gray_dark"
+        app:layout_constraintTop_toBottomOf="@id/llPlayerHeader"/>
+
+    <TextView
+        android:id="@+id/tvCurrentProgress"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="15dp"
+        android:text="1:20:37"
+        android:textColor="@color/turquoise"
+        android:textSize="13dp"
+        app:layout_constraintStart_toStartOf="@+id/sbPlayerProgress"
+        app:layout_constraintTop_toBottomOf="@id/vPlayerSeparator"/>
+
+    <TextView
+        android:id="@+id/tvTotalProgress"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="15dp"
+        android:text="2:20:31"
+        android:textColor="@color/turquoise"
+        android:textSize="13dp"
+        app:layout_constraintEnd_toEndOf="@+id/sbPlayerProgress"
+        app:layout_constraintTop_toBottomOf="@id/vPlayerSeparator"/>
+
+    <android.support.v7.widget.AppCompatSeekBar
+        android:id="@+id/sbPlayerProgress"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="35dp"
+        android:layout_marginRight="35dp"
+        android:maxHeight="3dp"
+        android:minHeight="2dp"
+        android:progressDrawable="@drawable/seekbar_progress"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tvCurrentProgress"/>
+
+    <LinearLayout
+        android:id="@+id/llPlayerButtons"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/sbPlayerProgress">
+
+        <ImageButton
+            android:id="@+id/ibRewind"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@null"
+            android:padding="@dimen/player_button_padding"
+            android:src="@drawable/prev_con_selector"
+            tools:ignore="ContentDescription"/>
+
+        <ImageButton
+            android:id="@+id/ibPlayPause"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@null"
+            android:padding="@dimen/player_button_padding"
+            android:src="@drawable/play_selector"
+            tools:ignore="ContentDescription"/>
+
+        <ImageButton
+            android:id="@+id/ibFastForward"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@null"
+            android:padding="@dimen/player_button_padding"
+            android:src="@drawable/next_icon_selector"
+            tools:ignore="ContentDescription"/>
+
+    </LinearLayout>
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_player_header.xml b/Android/app/src/main/res/layout/fragment_player_header.xml
new file mode 100644 (file)
index 0000000..a10dd39
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/colorPrimary"
+    tools:ignore="SpUsage">
+
+    <ImageView
+        android:id="@+id/ivCoverBackground"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:contentDescription="@string/app_name"
+        android:scaleType="centerCrop"/>
+
+    <View
+        android:id="@+id/vCoverOverlay"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <ImageButton
+        android:id="@+id/ibBack"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="16dp"
+        android:background="?attr/selectableItemBackgroundBorderless"
+        android:src="@drawable/ic_prev_tint"
+        android:tint="@color/white"
+        tools:ignore="ContentDescription"/>
+
+    <TextView
+        android:id="@+id/tvAuthor"
+        style="@style/BookHeaderTextView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/ibBack"
+        android:layout_marginEnd="24dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginTop="20dp"
+        android:text="Adam Mickiewicz"
+        android:textSize="18dp"/>
+
+    <TextView
+        android:id="@+id/tvBookTitle"
+        style="@style/BookHeaderTextView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/tvAuthor"
+        android:layout_marginEnd="24dp"
+        android:layout_marginStart="24dp"
+        android:text="Dziady"
+        android:textSize="26dp"/>
+
+    <ImageView
+        android:id="@+id/ivCover"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_below="@id/tvBookTitle"
+        android:layout_marginTop="20dp"
+        android:adjustViewBounds="true"
+        android:src="@drawable/list_nocover"
+        tools:ignore="ContentDescription"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_player_playlist.xml b/Android/app/src/main/res/layout/fragment_player_playlist.xml
new file mode 100644 (file)
index 0000000..1e55c63
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/colorPrimary">
+
+    <com.moiseum.wolnelektury.components.ProgressRecyclerView
+        android:id="@+id/rvPlayerPlaylist"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_search.xml b/Android/app/src/main/res/layout/fragment_search.xml
new file mode 100644 (file)
index 0000000..1e470ae
--- /dev/null
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:tools="http://schemas.android.com/tools"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:animateLayoutChanges="true"
+              android:orientation="vertical">
+
+    <RelativeLayout
+        android:id="@+id/rlFiltersContainer"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/book_search_filters_height"
+        android:background="@color/turquoise_dark"
+        android:visibility="gone">
+
+        <ImageButton
+            android:id="@+id/ibClearFilters"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentEnd="true"
+            android:layout_centerVertical="true"
+            android:layout_marginLeft="10dp"
+            android:layout_marginRight="10dp"
+            android:background="@drawable/close_filters_selector"
+            android:padding="12dp"
+            android:src="@drawable/close_small_dark_tint"
+            tools:ignore="ContentDescription"/>
+
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/rvFilters"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_toStartOf="@id/ibClearFilters"/>
+    </RelativeLayout>
+
+    <RelativeLayout
+        android:animateLayoutChanges="true"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.moiseum.wolnelektury.components.ProgressRecyclerView
+            android:id="@+id/rvBooks"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+        <ProgressBar
+            android:id="@+id/pbLoadMore"
+            style="@style/Base.Widget.AppCompat.ProgressBar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="10dp"
+            android:visibility="gone"/>
+
+        <Button
+            android:id="@+id/btnReloadMore"
+            style="@style/RoundedButton.WhiteBorder"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="10dp"
+            android:text="@string/load_again"
+            android:visibility="gone"/>
+    </RelativeLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_settings.xml b/Android/app/src/main/res/layout/fragment_settings.xml
new file mode 100644 (file)
index 0000000..22eea0e
--- /dev/null
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/colorPrimaryDark">
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                  xmlns:app="http://schemas.android.com/apk/res-auto"
+                  xmlns:tools="http://schemas.android.com/tools"
+                  android:layout_width="match_parent"
+                  android:layout_height="match_parent"
+                  android:orientation="vertical">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingBottom="@dimen/settings_bottom_padding"
+            android:paddingEnd="@dimen/settings_padding"
+            android:paddingStart="@dimen/settings_padding"
+            android:paddingTop="@dimen/settings_padding">
+
+            <android.support.v7.widget.SwitchCompat
+                android:id="@+id/swNotifications"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/settings_notifications"
+                android:textAllCaps="true"
+                android:textColor="@color/white"
+                android:textSize="@dimen/settings_text_size"
+                app:theme="@style/DarkBackgroundSwitch"
+                tools:ignore="RtlHardcoded,SpUsage"/>
+
+        </LinearLayout>
+
+        <View
+            android:id="@+id/first_separator"
+            style="@style/SettingsSeparator"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"/>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingBottom="@dimen/settings_bottom_padding"
+            android:paddingEnd="@dimen/settings_padding"
+            android:paddingStart="@dimen/settings_padding"
+            android:paddingTop="@dimen/settings_bottom_padding">
+
+            <TextView
+                android:id="@+id/tv_subscribtion"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_centerVertical="true"
+                android:layout_marginEnd="15dp"
+                android:layout_toStartOf="@id/tvState"
+                android:text="@string/subscribtion_state"
+                android:textAllCaps="true"
+                android:textColor="@color/white"
+                android:textSize="@dimen/settings_text_size"/>
+
+            <TextView
+                android:id="@+id/tvState"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:layout_centerVertical="true"
+                android:text="@string/inactive"
+                android:textAllCaps="true"
+                android:textColor="@color/white"
+                android:textSize="@dimen/settings_text_size"/>
+
+        </RelativeLayout>
+
+        <View
+            android:id="@+id/second_separator"
+            style="@style/SettingsSeparator"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"/>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingBottom="@dimen/settings_bottom_padding"
+            android:paddingEnd="@dimen/settings_padding"
+            android:paddingStart="@dimen/settings_padding"
+            android:paddingTop="@dimen/settings_bottom_padding">
+
+            <TextView
+                android:id="@+id/tv_delete"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_centerVertical="true"
+                android:layout_marginEnd="15dp"
+                android:layout_toStartOf="@id/btnDelete"
+                android:text="@string/delete_files"
+                android:textAllCaps="true"
+                android:textColor="@color/white"
+                android:textSize="@dimen/settings_text_size"/>
+
+            <Button
+                android:id="@+id/btnDelete"
+                style="@style/RoundedButton.WhiteBorder"
+                android:layout_alignParentEnd="true"
+                android:layout_centerVertical="true"
+                android:layout_weight="0"
+                android:text="@string/delete"/>
+
+        </RelativeLayout>
+
+    </LinearLayout>
+
+    <Button
+        android:id="@+id/btnBecomeAFriend"
+        style="@style/RoundedButton.Orange"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="15dp"
+        android:text="@string/become_a_friend"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_single_news.xml b/Android/app/src/main/res/layout/fragment_single_news.xml
new file mode 100644 (file)
index 0000000..75c552e
--- /dev/null
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout android:id="@+id/clMainView"
+                                                 xmlns:android="http://schemas.android.com/apk/res/android"
+                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
+                                                 android:layout_width="match_parent"
+                                                 android:layout_height="match_parent">
+
+    <android.support.design.widget.AppBarLayout
+        android:id="@+id/app_bar_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
+
+        <android.support.design.widget.CollapsingToolbarLayout
+            android:id="@+id/ctlCollapse"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/news_header_height"
+            app:contentScrim="?attr/colorPrimary"
+            app:expandedTitleTextAppearance="@android:color/transparent"
+            app:layout_scrollFlags="snap">
+
+            <include
+                layout="@layout/fragment_single_news_header"
+                app:layout_collapseMode="parallax"/>
+
+            <android.support.v7.widget.Toolbar
+                android:id="@+id/bookToolbar"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                android:minHeight="?attr/actionBarSize"
+                app:layout_collapseMode="pin"
+                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
+
+        </android.support.design.widget.CollapsingToolbarLayout>
+
+    </android.support.design.widget.AppBarLayout>
+
+    <android.support.v4.widget.NestedScrollView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="fill_vertical"
+        android:fillViewport="true"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+        <LinearLayout
+            android:id="@+id/llContentContainer"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:padding="20dp">
+
+            <TextView
+                android:id="@+id/tvNewsTitle"
+                style="@style/NewsHeaderTextView"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="15dp"/>
+
+            <TextView
+                style="@style/NewsText.Black"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/news_when"/>
+
+            <TextView
+                android:id="@+id/tvNewsTime"
+                style="@style/NewsText.TurquoiseBold"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"/>
+
+            <TextView
+                style="@style/NewsText.Black"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/news_where"/>
+
+            <TextView
+                android:id="@+id/tvNewsPlace"
+                style="@style/NewsText.TurquoiseBold"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"/>
+
+            <org.sufficientlysecure.htmltextview.HtmlTextView
+                android:id="@+id/tvNewsBody"
+                style="@style/NewsText.Black"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="?attr/actionBarSize"
+                android:layout_marginTop="15dp"/>
+
+        </LinearLayout>
+
+    </android.support.v4.widget.NestedScrollView>
+
+    <android.support.design.widget.FloatingActionButton
+        android:id="@+id/fabShare"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="25dp"
+        android:src="@drawable/ic_share"
+        android:tint="@color/orange"
+        app:backgroundTint="@color/white"
+        app:elevation="6dp"
+        app:layout_anchor="@id/app_bar_layout"
+        app:layout_anchorGravity="bottom|end"
+        app:pressedTranslationZ="8dp"/>
+
+</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_single_news_gallery_item.xml b/Android/app/src/main/res/layout/fragment_single_news_gallery_item.xml
new file mode 100644 (file)
index 0000000..4d79d5f
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent">
+
+    <ProgressBar
+        android:id="@+id/pbImageLoading"
+        style="@style/Base.Widget.AppCompat.ProgressBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"/>
+
+    <ImageView
+        android:id="@+id/tvGalleryImage"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"
+        tools:ignore="ContentDescription"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_single_news_header.xml b/Android/app/src/main/res/layout/fragment_single_news_header.xml
new file mode 100644 (file)
index 0000000..7b756d8
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/news_header_height">
+
+    <android.support.v4.view.ViewPager
+        android:id="@+id/vpGallery"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/news_header_height"/>
+
+    <me.relex.circleindicator.CircleIndicator
+        android:id="@+id/indicator"
+        android:layout_width="match_parent"
+        android:layout_height="48dp"
+        android:layout_alignParentBottom="true"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_support_us.xml b/Android/app/src/main/res/layout/fragment_support_us.xml
new file mode 100644 (file)
index 0000000..aa0623b
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:tools="http://schemas.android.com/tools"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            tools:ignore="SpUsage, ContentDescription">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:padding="30dp">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_marginBottom="10dp"
+            android:src="@drawable/logo_wl_light"/>
+
+        <TextView
+            android:id="@+id/tvSupportUsText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="20dp"
+            android:text="@string/support_us_text"
+            android:textColor="@color/gray_dark"
+            android:textSize="16dp"
+            tools:ignore="SpUsage"/>
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/support_us_header"
+            android:textColor="@color/gray_dark"
+            android:textSize="16dp"
+            tools:ignore="SpUsage"/>
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_marginTop="20dp"
+            android:src="@drawable/logo_opp"/>
+
+    </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/fragment_web_view.xml b/Android/app/src/main/res/layout/fragment_web_view.xml
new file mode 100644 (file)
index 0000000..a6d855e
--- /dev/null
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    android:id="@+id/activity_home"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:id="@+id/llButtonPanel"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:background="@color/colorPrimary"
+        android:orientation="horizontal"
+        android:paddingBottom="2dp"
+        android:paddingTop="2dp">
+
+        <Button
+            android:id="@+id/btnBack"
+            style="@style/FlatButton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:drawablePadding="2dp"
+            android:drawableTop="@drawable/ic_arrow_back_white_24dp"
+            android:enabled="false"
+            android:paddingTop="8dp"
+            android:text="@string/back"
+            android:textColor="@color/white"
+            android:textSize="9dp"/>
+
+
+        <Button
+            android:id="@+id/btnRefresh"
+            style="@style/FlatButton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:drawablePadding="2dp"
+            android:drawableTop="@drawable/ic_refresh_white_24dp"
+            android:paddingTop="8dp"
+            android:text="@string/refresh"
+            android:textColor="@color/white"
+            android:textSize="9dp"/>
+
+        <Button
+            android:id="@+id/btnNext"
+            style="@style/FlatButton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:drawablePadding="2dp"
+            android:drawableTop="@drawable/ic_arrow_forward_white_24dp"
+            android:enabled="false"
+            android:paddingTop="8dp"
+            android:text="@string/forward"
+            android:textColor="@color/white"
+            android:textSize="9dp"/>
+
+    </LinearLayout>
+
+    <WebView
+        android:id="@+id/wvAbout"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_above="@+id/llButtonPanel"/>
+
+    <TextView
+        android:id="@+id/tvPageError"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_above="@+id/llButtonPanel"
+        android:layout_centerInParent="true"
+        android:background="@color/white"
+        android:gravity="center"
+        android:text="@string/page_error"
+        android:textColor="@color/black"
+        android:visibility="gone"/>
+
+</RelativeLayout>
diff --git a/Android/app/src/main/res/layout/fragment_zoom.xml b/Android/app/src/main/res/layout/fragment_zoom.xml
new file mode 100644 (file)
index 0000000..ffd42d0
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/colorPrimary">
+
+    <com.moiseum.wolnelektury.components.ZoomableViewPager
+        android:id="@+id/vpGallery"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <me.relex.circleindicator.CircleIndicator
+        android:id="@+id/indicator"
+        android:layout_width="match_parent"
+        android:layout_height="48dp"
+        android:layout_alignParentBottom="true"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/library_header.xml b/Android/app/src/main/res/layout/library_header.xml
new file mode 100644 (file)
index 0000000..214e9d9
--- /dev/null
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:animateLayoutChanges="true">
+
+    <LinearLayout
+        android:id="@+id/linearLayout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@color/splash_background"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        android:paddingBottom="10dp"
+        android:paddingEnd="10dp"
+        android:paddingStart="10dp">
+
+        <android.support.v7.widget.CardView
+            android:layout_width="@dimen/thumb_size"
+            android:layout_height="wrap_content"
+            app:cardCornerRadius="@dimen/thumb_corners">
+
+            <ImageView
+                android:id="@+id/ivBookCover"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:adjustViewBounds="true"
+                android:scaleType="fitXY"
+                android:src="@drawable/list_nocover"
+                tools:ignore="ContentDescription"/>
+        </android.support.v7.widget.CardView>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:paddingEnd="10dp"
+            android:paddingStart="10dp">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <TextView
+                    android:id="@+id/tvBookAuthor"
+                    style="@style/ListTitleText.Black"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_toStartOf="@id/ibDeleteEbook"
+                    android:text="Juliusz Słowacki"
+                    android:textColor="@color/white"/>
+
+                <TextView
+                    android:id="@+id/tvBookTitle"
+                    style="@style/ListHeaderText"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_below="@id/tvBookAuthor"
+                    android:layout_toStartOf="@id/ibDeleteEbook"
+                    android:text="Kordian"
+                    android:textColor="@color/orange_light"/>
+
+            </RelativeLayout>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_marginBottom="5dp"
+                android:layout_marginTop="5dp"
+                android:background="@color/white"/>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <ImageView
+                    android:id="@+id/ivEbook"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentStart="true"
+                    android:layout_alignParentTop="true"
+                    app:srcCompat="@drawable/ic_glass_mid_tint_white"
+                    tools:ignore="ContentDescription"/>
+
+                <ImageView
+                    android:id="@+id/ivAudiobook"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerVertical="true"
+                    android:layout_marginStart="10dp"
+                    android:layout_toEndOf="@id/ivEbook"
+                    app:srcCompat="@drawable/ic_speaker_mid_tint_white"
+                    tools:ignore="ContentDescription"/>
+
+            </RelativeLayout>
+
+            <TableLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <TableRow>
+
+                    <TextView
+                        style="@style/ListTableTitleText"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="1"
+                        android:text="@string/book_epoch"
+                        android:textColor="@color/white"
+                        android:textSize="8dp"/>
+
+                    <TextView
+                        android:id="@+id/tvBookEpoch"
+                        style="@style/ListTitleText.Orange"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="7"
+                        android:text="Romantyzm"/>
+                </TableRow>
+
+                <TableRow>
+
+                    <TextView
+                        style="@style/ListTableTitleText"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="1"
+                        android:text="@string/book_kind"
+                        android:textColor="@color/white"
+                        android:textSize="8dp"/>
+
+                    <TextView
+                        android:id="@+id/tvBookKind"
+                        style="@style/ListTitleText.Orange"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="7"/>
+                </TableRow>
+
+                <TableRow>
+
+                    <TextView
+                        style="@style/ListTableTitleText"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="1"
+                        android:text="@string/book_genre"
+                        android:textColor="@color/white"
+                        android:textSize="8dp"/>
+
+                    <TextView
+                        android:id="@+id/tvBookGenre"
+                        style="@style/ListTitleText.Orange"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="7"/>
+                </TableRow>
+            </TableLayout>
+        </LinearLayout>
+    </LinearLayout>
+
+    <RelativeLayout
+        android:id="@+id/rlHeaderLoadingContainer"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:background="@color/splash_background"
+        app:layout_constraintBottom_toBottomOf="@+id/linearLayout"
+        app:layout_constraintTop_toTopOf="@+id/linearLayout">
+
+        <ProgressBar
+            android:id="@+id/pbHeaderLoading"
+            style="@style/Base.Widget.AppCompat.ProgressBar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:theme="@style/CircularProgress"/>
+
+        <TextView
+            android:id="@+id/tvEmpty"
+            style="@style/BookHeaderTextView"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center"
+            android:padding="20dp"
+            android:text="@string/no_prapremiere_message"
+            android:textSize="14sp"
+            android:visibility="gone"/>
+
+        <ImageButton
+            android:id="@+id/ibRetry"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:background="@null"
+            android:src="@drawable/refresh_icon_light_selector"
+            android:visibility="gone"
+            tools:ignore="ContentDescription"/>
+    </RelativeLayout>
+</android.support.constraint.ConstraintLayout>
+
diff --git a/Android/app/src/main/res/layout/list_search.xml b/Android/app/src/main/res/layout/list_search.xml
new file mode 100644 (file)
index 0000000..25612a0
--- /dev/null
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:app="http://schemas.android.com/apk/res-auto"
+              xmlns:tools="http://schemas.android.com/tools"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:gravity="center_vertical"
+              android:orientation="horizontal"
+              android:padding="10dp">
+
+    <android.support.v7.widget.CardView
+        android:layout_width="@dimen/thumb_size"
+        android:layout_height="wrap_content"
+        app:cardCornerRadius="@dimen/thumb_corners">
+
+        <ImageView
+            android:id="@+id/ivBookCover"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:adjustViewBounds="true"
+            android:scaleType="fitXY"
+            android:src="@drawable/list_nocover"
+            tools:ignore="ContentDescription"/>
+    </android.support.v7.widget.CardView>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:paddingEnd="10dp"
+        android:paddingStart="10dp">
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <ImageButton
+                android:id="@+id/ibDeleteEbook"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:layout_centerVertical="true"
+                android:background="@null"
+                android:padding="10dp"
+                android:src="@drawable/delete_icon_selector"
+                android:visibility="gone"
+                tools:ignore="ContentDescription"/>
+
+            <TextView
+                android:id="@+id/tvBookAuthor"
+                style="@style/ListTitleText.Black"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_toStartOf="@id/ibDeleteEbook"
+                android:text="Juliusz Słowacki"/>
+
+            <TextView
+                android:id="@+id/tvBookTitle"
+                style="@style/ListHeaderText"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_below="@id/tvBookAuthor"
+                android:layout_marginEnd="1dp"
+                android:layout_toStartOf="@id/ibDeleteEbook"
+                android:text="Kordian"/>
+        </RelativeLayout>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginBottom="5dp"
+            android:layout_marginTop="5dp"
+            android:background="@color/gray_dark"/>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="5dp"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:id="@+id/ivEbook"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:src="@drawable/ic_glass_mid"
+                android:tint="@color/gray_very_dark"
+                tools:ignore="ContentDescription"/>
+
+            <TextView
+                android:id="@+id/tvEbookReaden"
+                style="@style/ListTableTitleText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:layout_marginStart="@dimen/list_search_read_listen_status_margins"
+                android:text="Przeczytano 50%"
+                android:textAllCaps="false"
+                tools:ignore="ContentDescription"/>
+
+            <ImageView
+                android:id="@+id/ivAudioBook"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:layout_marginStart="@dimen/list_search_read_listen_status_margins"
+                android:src="@drawable/ic_speaker_mid"
+                android:tint="@color/gray_very_dark"
+                tools:ignore="ContentDescription"/>
+
+            <TextView
+                android:id="@+id/tvAudioBookReaden"
+                style="@style/ListTableTitleText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:layout_marginStart="@dimen/list_search_read_listen_status_margins"
+                android:text="Odsłuchano 50%"
+                android:textAllCaps="false"
+                tools:ignore="ContentDescription"/>
+        </LinearLayout>
+
+        <TableLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <TableRow>
+
+                <TextView
+                    style="@style/ListTableTitleText"
+                    android:layout_width="0px"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="2.6"
+                    android:text="@string/book_epoch"/>
+
+                <TextView
+                    android:id="@+id/tvBookEpoch"
+                    style="@style/ListTitleText.Turquoise"
+                    android:layout_width="0px"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="7"
+                    android:text="Romantyzm"/>
+            </TableRow>
+
+            <TableRow>
+
+                <TextView
+                    style="@style/ListTableTitleText"
+                    android:layout_width="0px"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="2.6"
+                    android:text="@string/book_kind"/>
+
+                <TextView
+                    android:id="@+id/tvBookKind"
+                    style="@style/ListTitleText.Turquoise"
+                    android:layout_width="0px"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="7"/>
+            </TableRow>
+
+            <TableRow>
+
+                <TextView
+                    style="@style/ListTableTitleText"
+                    android:layout_width="0px"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="2.6"
+                    android:text="@string/book_genre"/>
+
+                <TextView
+                    android:id="@+id/tvBookGenre"
+                    style="@style/ListTitleText.Turquoise"
+                    android:layout_width="0px"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="7"/>
+            </TableRow>
+        </TableLayout>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/navigation_blank.xml b/Android/app/src/main/res/layout/navigation_blank.xml
new file mode 100644 (file)
index 0000000..56e90dd
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+      android:layout_width="match_parent"
+      android:layout_height="0dp"/>
diff --git a/Android/app/src/main/res/layout/navigation_footer.xml b/Android/app/src/main/res/layout/navigation_footer.xml
new file mode 100644 (file)
index 0000000..57c721b
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout android:id="@+id/llProfileContainer"
+                xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true"
+                android:background="@color/colorPrimary"
+                android:padding="10dp"
+                tools:ignore="SpUsage">
+
+    <Button
+        android:id="@+id/btnLogin"
+        style="@style/RoundedButton.WhiteBorder"
+        android:layout_centerHorizontal="true"
+        android:text="@string/menu_login"/>
+
+    <LinearLayout
+        android:id="@+id/llLoggedInContainer"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:gravity="center_horizontal"
+        android:orientation="vertical"
+        android:visibility="gone">
+
+        <TextView
+            android:id="@+id/tvLogged"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/logged_as"
+            android:textAllCaps="true"
+            android:textColor="@color/white"
+            android:textSize="10dp"/>
+
+        <TextView
+            android:id="@+id/tvUsername"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/white"
+            android:textSize="14dp"/>
+
+        <Button
+            android:id="@+id/btnLogout"
+            style="@style/RoundedButton.WhiteBorder"
+            android:layout_marginTop="10dp"
+            android:text="@string/menu_logout"/>
+    </LinearLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/navigation_item.xml b/Android/app/src/main/res/layout/navigation_item.xml
new file mode 100644 (file)
index 0000000..d4996d9
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                                             xmlns:app="http://schemas.android.com/apk/res-auto"
+                                             android:layout_width="match_parent"
+                                             android:layout_height="wrap_content"
+                                             android:layout_marginBottom="5dp"
+                                             android:layout_marginEnd="20dp"
+                                             android:layout_marginStart="20dp"
+                                             android:layout_marginTop="5dp"
+                                             android:background="@drawable/nav_item_background_selector">
+
+    <TextView
+        android:id="@+id/tvNavName"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="20dp"
+        android:textColor="@color/white"
+        android:textSize="18dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/ivNavIcon"
+        app:layout_constraintHorizontal_bias="0.035"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.516"/>
+
+    <ImageView
+        android:id="@+id/ivNavIcon"
+        android:layout_width="20dp"
+        android:layout_height="20dp"
+        android:layout_marginEnd="10dp"
+        android:src="@drawable/ic_todo"
+        android:tint="@color/turquoise"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/navigation_separator_item.xml b/Android/app/src/main/res/layout/navigation_separator_item.xml
new file mode 100644 (file)
index 0000000..d3453bc
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+      android:layout_width="match_parent"
+      android:layout_height="1dp"
+      android:layout_marginBottom="13dp"
+      android:layout_marginEnd="25dp"
+      android:layout_marginStart="25dp"
+      android:layout_marginTop="13dp"
+      android:background="@color/turquoise"/>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/navigation_support_item.xml b/Android/app/src/main/res/layout/navigation_support_item.xml
new file mode 100644 (file)
index 0000000..dd95073
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:layout_marginBottom="15dp"
+              android:layout_marginTop="15dp"
+              android:gravity="center">
+
+    <Button
+        android:id="@+id/btnSupportUs"
+        style="@style/RoundedButton.Orange"
+        android:text="@string/become_a_friend"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/news_item.xml b/Android/app/src/main/res/layout/news_item.xml
new file mode 100644 (file)
index 0000000..90d0074
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:padding="10dp">
+
+    <android.support.v7.widget.CardView
+        android:id="@+id/cvNewsThumb"
+        android:layout_width="@dimen/thumb_size"
+        android:layout_height="@dimen/thumb_size"
+        android:layout_centerVertical="true"
+        app:cardCornerRadius="@dimen/thumb_corners">
+
+        <ImageView
+            android:id="@+id/ivNewsThumb"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:scaleType="centerCrop"
+            android:src="@drawable/list_nocover"
+            tools:ignore="ContentDescription"/>
+    </android.support.v7.widget.CardView>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/news_text_box_padding"
+        android:layout_toEndOf="@id/cvNewsThumb"
+        android:orientation="vertical"
+        android:paddingBottom="5dp"
+        android:paddingTop="5dp"
+        tools:ignore="SpUsage">
+
+        <TextView
+            android:id="@+id/textViewDate"
+            style="@style/NewsText.Black"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAllCaps="true"
+            android:textSize="15dp"/>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="@color/gray_dark"/>
+
+        <TextView
+            android:id="@+id/textViewLead"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/colorPrimary"
+            android:textSize="18dp"/>
+    </LinearLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/playlist_item.xml b/Android/app/src/main/res/layout/playlist_item.xml
new file mode 100644 (file)
index 0000000..c39b6ba
--- /dev/null
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/turquoise">
+
+    <TextView
+        android:id="@+id/tvMediaName"
+        style="@style/PlayerPlaylistText"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentStart="true"
+        android:layout_centerVertical="true"
+        android:layout_toStartOf="@id/ibPlay"
+        android:padding="10dp"/>
+
+    <ImageButton
+        android:id="@+id/ibPlay"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentEnd="true"
+        android:layout_centerVertical="true"
+        android:background="@null"
+        android:padding="20dp"
+        android:src="@drawable/play_white_tint"
+        android:visibility="invisible"
+        tools:ignore="ContentDescription"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/prapremiere_info.xml b/Android/app/src/main/res/layout/prapremiere_info.xml
new file mode 100644 (file)
index 0000000..455afcf
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    android:id="@+id/clPremium"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@color/colorPrimary"
+    android:gravity="center_horizontal"
+    android:orientation="vertical"
+    android:paddingBottom="@dimen/pra_premiere_top_bottom_padding"
+    android:paddingEnd="@dimen/pra_premiere_side_padding"
+    android:paddingStart="@dimen/pra_premiere_side_padding"
+    android:paddingTop="@dimen/pra_premiere_top_bottom_padding">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/prapremiere_info"
+        android:textAlignment="center"
+        android:textColor="@color/white"
+        android:textSize="16dp"
+        tools:ignore="SpUsage"/>
+
+    <Button
+        android:id="@+id/bSupportUs"
+        style="@style/RoundedButton.Orange"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_marginTop="20dp"
+        android:drawableEnd="@drawable/ic_menu_star_tint_white"
+        android:drawablePadding="10dp"
+        android:text="@string/become_a_friend"
+        android:textSize="16dp"
+        tools:ignore="SpUsage"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/progress_flowlayout.xml b/Android/app/src/main/res/layout/progress_flowlayout.xml
new file mode 100644 (file)
index 0000000..dbbf216
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:animateLayoutChanges="true"
+                android:background="@android:color/transparent">
+
+    <com.nex3z.flowlayout.FlowLayout
+        android:id="@+id/flList"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:animateLayoutChanges="true"
+        android:visibility="gone"
+        app:flChildSpacing="@dimen/filter_checkbox_spacing"
+        app:flChildSpacingForLastRow="align"
+        app:flRowSpacing="8dp"/>
+
+    <ProgressBar
+        android:id="@+id/pbLoading"
+        style="@style/Base.Widget.AppCompat.ProgressBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:layout_marginBottom="@dimen/flowlayout_progress_margin"
+        android:layout_marginTop="@dimen/flowlayout_progress_margin"
+        android:theme="@style/CircularProgress"/>
+
+    <ImageButton
+        android:id="@+id/ibRetry"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:layout_marginBottom="@dimen/flowlayout_progress_margin"
+        android:layout_marginTop="@dimen/flowlayout_progress_margin"
+        android:background="@null"
+        android:src="@drawable/refresh_icon_light_selector"
+        android:visibility="gone"
+        tools:ignore="ContentDescription"/>
+
+    <TextView
+        android:id="@+id/tvEmpty"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:layout_margin="10dp"
+        android:text="@string/no_results"
+        android:textColor="@color/gray_dark"
+        android:textSize="14dp"
+        android:visibility="gone"
+        tools:ignore="SpUsage"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/progress_recyclerview.xml b/Android/app/src/main/res/layout/progress_recyclerview.xml
new file mode 100644 (file)
index 0000000..53fa343
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                                             xmlns:app="http://schemas.android.com/apk/res-auto"
+                                             xmlns:tools="http://schemas.android.com/tools"
+                                             android:layout_width="match_parent"
+                                             android:layout_height="match_parent">
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/rvList"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <ProgressBar
+        android:id="@+id/pbLoading"
+        style="@style/Base.Widget.AppCompat.ProgressBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <ImageButton
+        android:id="@+id/ibRetry"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/refresh_icon_selector"
+        android:visibility="gone"
+        android:background="@null"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="ContentDescription"/>
+
+    <TextView
+        android:id="@+id/tvEmpty"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="10dp"
+        android:text="@string/no_results"
+        android:textColor="@color/gray_dark"
+        android:textSize="14dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/zoom_item.xml b/Android/app/src/main/res/layout/zoom_item.xml
new file mode 100644 (file)
index 0000000..0df8cee
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/colorPrimary">
+
+    <it.sephiroth.android.library.imagezoom.ImageViewTouch
+        android:id="@+id/ivPointPhoto"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        tools:ignore="ContentDescription"/>
+
+    <ProgressBar
+        android:id="@+id/pbLoading"
+        style="@style/Base.Widget.AppCompat.ProgressBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:theme="@style/CircularProgress"/>
+
+    <TextView
+        android:id="@+id/tvLoading"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/pbLoading"
+        android:layout_centerHorizontal="true"
+        android:layout_marginTop="5dp"
+        android:text="@string/loading"
+        android:textAllCaps="true"
+        android:textColor="@color/white"
+        android:textSize="16dp"
+        tools:ignore="SpUsage"/>
+
+    <Button
+        android:id="@+id/btnRetry"
+        style="@style/RoundedButton.WhiteBorder"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:paddingLeft="50dp"
+        android:paddingRight="50dp"
+        android:text="@string/load_again"
+        android:visibility="gone"/>
+
+</RelativeLayout>
diff --git a/Android/app/src/main/res/menu/menu_filter.xml b/Android/app/src/main/res/menu/menu_filter.xml
new file mode 100644 (file)
index 0000000..b9b6f69
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item
+        android:id="@+id/action_accept"
+        android:icon="@drawable/ic_accept_new"
+        android:title="@string/menu_accept"
+        app:showAsAction="always"/>
+</menu>
\ No newline at end of file
diff --git a/Android/app/src/main/res/menu/menu_search.xml b/Android/app/src/main/res/menu/menu_search.xml
new file mode 100644 (file)
index 0000000..04d97c4
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto"
+      xmlns:tools="http://schemas.android.com/tools"
+      tools:ignore="AlwaysShowAction">
+    <item
+        android:id="@+id/action_search"
+        android:icon="@drawable/ic_search_new"
+        android:title="@string/menu_search"
+        app:actionViewClass="android.support.v7.widget.SearchView"
+        app:showAsAction="collapseActionView|always"/>
+
+    <item
+        android:id="@+id/action_filter"
+        android:icon="@drawable/ic_filter_new"
+        android:title="@string/menu_filter"
+        app:showAsAction="always"/>
+</menu>
\ No newline at end of file
diff --git a/Android/app/src/main/res/menu/menu_searchable.xml b/Android/app/src/main/res/menu/menu_searchable.xml
new file mode 100644 (file)
index 0000000..1f06293
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_search"
+        android:icon="@drawable/ic_search_new"
+        android:title="@string/menu_search"
+        app:showAsAction="always"/>
+
+</menu>
\ No newline at end of file
diff --git a/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..3bc80d3
Binary files /dev/null and b/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..d8c75a8
Binary files /dev/null and b/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..ae135d4
Binary files /dev/null and b/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..f1604e4
Binary files /dev/null and b/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..0a723fd
Binary files /dev/null and b/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/Android/app/src/main/res/values-v21/styles.xml b/Android/app/src/main/res/values-v21/styles.xml
new file mode 100644 (file)
index 0000000..32f4f7e
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="FlatButton" parent="android:style/Widget.Material.Button.Borderless.Small">
+        <item name="android:background">?android:attr/selectableItemBackgroundBorderless</item>
+        <item name="android:textColor">@color/gray_dark</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textSize">13dp</item>
+        <item name="android:textStyle">normal</item>
+    </style>
+
+    <style name="FlatButton.Separator" parent="android:style/Widget.Material.Button.Borderless.Small">
+        <item name="android:drawableRight">@drawable/ic_arrow_right_24dp</item>
+        <item name="android:textColor">@color/gray_dark</item>
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/Android/app/src/main/res/values/attrs.xml b/Android/app/src/main/res/values/attrs.xml
new file mode 100644 (file)
index 0000000..c8e704d
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="ProgressRecyclerView">
+        <attr name="emptyText" format="string"/>
+    </declare-styleable>
+    <declare-styleable name="FiltersProgressFlowLayout">
+        <attr name="emptyLayoutText" format="string"/>
+    </declare-styleable>
+    <declare-styleable name="ProgressDownloadButton">
+        <attr name="text_initial" format="string"/>
+        <attr name="text_downloaded" format="string"/>
+        <attr name="text_color" format="color"/>
+        <attr name="text_inverted_color" format="color"/>
+        <attr name="text_size" format="dimension"/>
+        <attr name="border_size" format="dimension"/>
+        <attr name="corner_radius" format="dimension"/>
+        <attr name="drawable" format="integer" />
+    </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/Android/app/src/main/res/values/colors.xml b/Android/app/src/main/res/values/colors.xml
new file mode 100644 (file)
index 0000000..2cb390a
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#206F76</color>
+    <color name="colorPrimaryDark">#19555B</color>
+    <color name="colorAccent">#19555B</color>
+
+    <color name="turquoise">#206F76</color>
+    <color name="turquoise_dark">#19555B</color>
+    <color name="turquoise_light">#349fa8</color>
+    <color name="white">#ffffff</color>
+    <color name="gray_dark">#414141</color>
+    <color name="gray_very_dark">#525252</color>
+    <color name="orange">#db4b16</color>
+    <color name="orange_light">#F49509</color>
+    <color name="orange_overlay">#80db4b16</color>
+    <color name="red_dark">#9e2b00</color>
+    <color name="placeholder_gray">#eeeeee</color>
+    <color name="audiobook_gray">#118990</color>
+    <color name="splash_background">#018189</color>
+</resources>
diff --git a/Android/app/src/main/res/values/dimens.xml b/Android/app/src/main/res/values/dimens.xml
new file mode 100644 (file)
index 0000000..72febd6
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <dimen name="separator_padding">5dp</dimen>
+    <dimen name="book_header_height">200dp</dimen>
+    <dimen name="book_header_top">25dp</dimen>
+
+    <dimen name="download_button_text_size_default">14dp</dimen>
+    <dimen name="download_button_border_size_default">2dp</dimen>
+    <dimen name="download_button_corner_radius_default">5dp</dimen>
+    <dimen name="book_details_padding">20dp</dimen>
+    <dimen name="book_details_button_margin">50dp</dimen>
+    <dimen name="book_details_corner_radius">10dp</dimen>
+    <dimen name="book_button_padding">20dp</dimen>
+    <dimen name="filter_checkbox_spacing">15dp</dimen>
+    <dimen name="filters_side_padding">10dp</dimen>
+    <dimen name="filters_header_spacing">10dp</dimen>
+    <dimen name="flowlayout_progress_margin">60dp</dimen>
+    <dimen name="book_search_filters_height">50dp</dimen>
+    <dimen name="book_button_text_size">18dp</dimen>
+    <dimen name="rounded_buttons_border_width">1dp</dimen>
+    <dimen name="list_title_text_size">15dp</dimen>
+    <dimen name="list_search_read_listen_status_margins">5dp</dimen>
+    <dimen name="news_text_box_padding">10dp</dimen>
+    <dimen name="news_header_height">250dp</dimen>
+    <dimen name="settings_padding">20dp</dimen>
+    <dimen name="settings_bottom_padding">10dp</dimen>
+    <dimen name="settings_text_size">16dp</dimen>
+    <dimen name="pra_premiere_side_padding">20dp</dimen>
+    <dimen name="pra_premiere_top_bottom_padding">35dp</dimen>
+    <dimen name="thumb_size">110dp</dimen>
+    <dimen name="thumb_corners">8dp</dimen>
+    <dimen name="player_button_padding">20dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml
new file mode 100644 (file)
index 0000000..14d862c
--- /dev/null
@@ -0,0 +1,153 @@
+<resources>
+    <string name="app_name">Wolne Lektury</string>
+    <string name="nav_wolne_lektury">Wolne Lektury</string>
+    <string name="nav_catalog">Biblioteka</string>
+    <string name="nav_news">Aktualności</string>
+    <string name="nav_my_collection">Moja kolekcja</string>
+    <string name="nav_about">O aplikacji</string>
+    <string name="nav_reading">Teraz czytam</string>
+    <string name="nav_completed">Przeczytane</string>
+    <string name="see_all">Zobacz wszystkie</string>
+    <string name="open">Otwórz</string>
+    <string name="close">Zamknij</string>
+    <string name="no_results">Brak wyników</string>
+    <string name="loading_results_failed">Wystąpił błąd podczas ładowania danych</string>
+    <string name="no_search_results">Brak rezultatów wyszukiwania</string>
+    <string name="book_epoch">Epoka</string>
+    <string name="book_kind">Rodzaj</string>
+    <string name="book_genre">Gatunek</string>
+    <string name="library_newest">Nowości</string>
+    <string name="library_recommended">Polecane</string>
+    <string name="library_my_collection">Moja kolekcja</string>
+    <string name="library_audiobooks">Audiobooki</string>
+    <string name="support_us">Wesprzyj nas</string>
+    <string name="pages">Stron</string>
+    <string name="back">Powrót</string>
+    <string name="refresh">Odśwież</string>
+    <string name="forward">Do przodu</string>
+    <string name="page_error">Nie udało się załadować zawartości.\nOdśwież stronę.</string>
+    <string name="download_ebook">Czytaj</string>
+    <string name="download_ebook_read">Czytaj dalej</string>
+    <string name="download_ebook_loading">Pobieranie</string>
+    <string name="download_audiobook">Słuchaj</string>
+    <string name="download_audiobook_read">Słuchaj dalej</string>
+    <string name="download_audiobook_loading">Pobieranie</string>
+    <string name="menu_filter">Filtruj</string>
+    <string name="menu_search">Wyszukaj</string>
+    <string name="only_lecture">Lektury szkolne</string>
+    <string name="filter_epochs">Epoki</string>
+    <string name="filter_genres">Gatunki</string>
+    <string name="filter_kinds">Rodzaje</string>
+    <string name="filters">Filtrowanie</string>
+    <string name="menu_accept">Potwierdź</string>
+    <string name="lectures">Lektury</string>
+    <string name="book_loading_error">Nie udało się załadować danych. Proszę spróbować ponownie.</string>
+    <string name="book_download_error">Nie udało się pobrać pliku ebook\'a. Proszę spróbować ponownie.</string>
+    <string name="share">Udostępnij</string>
+    <string name="load_epochs_failed">Nie udało się załadować epok</string>
+    <string name="load_genres_failed">Nie udało się załadować gatunków</string>
+    <string name="load_kinds_failed">Nie udało się załadować rodzajów</string>
+    <string name="load_player_failed">Nie udało się załadować odtwarzacza</string>
+    <string name="about_text"><![CDATA[Nie potrzebujesz już dostępu do komputera, żeby skorzystać z zasobów biblioteki
+        WolneLektury.pl! Aplikacja mobilna daje Ci dostęp do kilku tysięcy bezpłatnych utworów oraz kilkuset
+        audiobooków, zawsze tam, gdzie masz ochotę na przeczytanie książki!<br /><br />Wolne Lektury są najpopularniejszą w
+        Polsce biblioteką internetową. Od 2007 roku publikujemy utwory należące do klasyki literatury oraz literaturę
+        współczesną i udostępniamy je za darmo wszystkim zainteresowanym – znajdą tutaj dla siebie coś zarówno
+        dzieci, młodzież, jak i dorośli. Dzięki aplikacji czytelnicy uzyskują bezpośredni i wygodny dostęp na
+        urządzeniach mobilnych do zasobów biblioteki.<br /><br />Wszystkie utwory zamieszczone w bibliotece Wolne Lektury można
+        zgodnie z prawem bezpłatnie przeglądać, słuchać, pobierać na swój komputer, a także udostępniać innym i
+        cytować. Większość utworów nie jest objęta majątkowym prawem autorskim i znajduje się w
+        <a href="https://domenapubliczna.org/co-to-jest-domena-publiczna/">domenie publicznej</a>, co oznacza że można
+        je swobodnie publikować i rozpowszechniać. Pozostałe zostały udostępnione na jednej z wolnych licencji -
+        <a href="https://creativecommons.org/licenses/by-sa/3.0/">Creative Commons</a> Uznanie Autorstwa -
+        Na Tych Samych Warunkach 3.0.PL lub <a href="http://artlibre.org/licence/lal/pl/">Licencja Wolnej Sztuki 1.3</a>.<br /><br />
+        Zapoznaj się z <a href="https://wolnelektury.pl/info/regulamin/">Regulaminem biblioteki</a> oraz
+        <a href="https://nowoczesnapolska.org.pl/prywatnosc/">Polityką prywatności</a>.<br /><br />
+        Znajdziesz nas również pod adresem <a href="www.wolnelektury.pl">www.wolnelektury.pl</a>.<br /><br />
+        Kontakt: <a href="mailto:wolnelektury@nowoczesnapolska.org.pl">wolnelektury@nowoczesnapolska.org.pl</a>]]>
+    </string>
+    <string name="about_text_fundation">Bibliotekę prowadzi fundacja Nowoczesna Polska.</string>
+    <string name="about_text_mkdn">Dofinansowano ze środków Ministra Kultury i Dziedzictwa Narodowego.</string>
+
+    <string name="support_us_header">Podaruj nam swój 1%! Fundacja Nowoczesna Polska posiada status
+Organizacji Pożytku Publicznego dzięki czemu na rozwój projektu Wolne
+Lektury można przekazać 1% swojego podatku. W tym celu należy wypełnić
+odpowiednią rubrykę w rocznym zeznaniu podatkowym (PIT-36, PIT-37 lub
+PIT-28). W zeznaniu należy podać nazwę: Fundacja Nowoczesna Polska
+oraz numer z Krajowego Rejestru Sądowego (KRS) 0000070056.</string>
+    <string name="support_us_text"><![CDATA[Przekaż darowiznę na Wolne Lektury! Każda wpłacona kwota zostanie
+przeznaczona na rozwój zasobów naszej biblioteki. Więcej informacji
+znajdziesz na <a href="https://wolnelektury.pl/info/wesprzyj-nas/">naszej stronie</a>.<br/><br/>
+         Pomóż uwolnić konkretną książkę, wspierając zbiórkę na <a href="https://wolnelektury.pl/wesprzyj/lektura/">tej stronie</a>.]]></string>
+    <string name="login_request_token_failed">Pierwsza faza logowania nie powiodła się. Spróbuj ponownie.</string>
+    <string name="login_access_token_failed">Druga faza logowania nie powiodła się. Spróbuj ponownie.</string>
+    <string name="menu_login">Zaloguj się</string>
+    <string name="main_view_progress">Trwa ładowanie danych…</string>
+    <string name="login_auth_callback_malformed">Przepraszamy, coś poszło nie tak. Otrzymaliśmy błędną odpowiedź z serwera. Prosimy o kontakt z administracją.</string>
+    <string name="logged_as">Zalogowany jako</string>
+    <string name="menu_logout">Wyloguj</string>
+    <string name="logout_successful">Wylogowano</string>
+    <string name="unauthorized">Wygasły dane logowania. Proszę zalogować się ponownie.</string>
+    <string name="nav_premium">Prapremiera</string>
+    <string name="nav_favourites">Ulubione</string>
+    <string name="nav_audiobooks">Audiobooki</string>
+    <string name="audiobook">Audiobook</string>
+    <string name="has_audiobook">Audiobook</string>
+    <string name="nav_downloaded">Pobrane</string>
+    <string name="become_a_friend">Zostań przyjacielem</string>
+    <string name="reading_progress">Przeczytano %d%%</string>
+    <string name="listening_progress">Odsłuchano %d%%</string>
+    <string name="book_list_newest_title">Nowości</string>
+    <string name="book_list_recommended_title">Polecane</string>
+    <string name="player_timer_null">0:00</string>
+    <string name="news_when">Kiedy:</string>
+    <string name="news_where">Gdzie:</string>
+    <string name="retry">Spróbuj ponownie</string>
+    <string name="load_again">Załaduj ponownie</string>
+    <string name="prapremiere_info">Powyższe dzieło dostępne jest w ramach programu Wczesnego Dostępu dla
+        użytkowników, którzy zdecydowali się wesprzeć Wolne Lektury. Chcesz czytać już teraz? Wesprzyj nas! Zostań naszym przyjacielem!</string>
+    <string name="no_prapremiere_message">Obecnie przygotowujemy dla Ciebie nową Prapremierę. Jeśli chcesz mieć do niej
+        dostęp kliknij tutaj i zostań naszym Przyjacielem</string>
+    <string name="no_prapremiere_message_logged">Obecnie przygotowujemy dla Ciebie nową Prapremierę.</string>
+    <string name="player_chapter_number">Rozdział %02d</string>
+    <string name="label_stop">Stop</string>
+    <string name="label_pause">Pauza</string>
+    <string name="label_play">Odtwórz</string>
+    <string name="label_previous">Poprzedni</string>
+    <string name="label_next">Następny</string>
+    <string name="settings_notifications">Zezwalaj na powiadomienia</string>
+    <string name="subscribtion_state">Stan subskrybcji</string>
+    <string name="delete_files">Usuń wszystkie pobrane pliki</string>
+    <string name="settings">Ustawienia</string>
+    <string name="active">Aktywny</string>
+    <string name="inactive">Nieaktywny</string>
+    <string name="delete">Usuń</string>
+    <string name="premium_purchase_failed">Płatność nie powiodła się. Prosimy o kontakt z administracją.</string>
+    <string name="premium_purchase_succeeded">Od tej pory jesteś naszym subskrybentem! Dziękujemy!</string>
+    <string name="login_first">Aby zostać naszym przyjacielem musisz się najpierw zalogować</string>
+    <string name="default_notification_channel_id">wolne_lektury_default_channel_id</string>
+    <string name="default_notification_topic">wolnelektury</string>
+    <string name="fetching_premium_failed">Nie udało się pobrać informacji o prapremierze. Spróbuj jeszcze raz!</string>
+    <string name="book_deleted_message">Pozycja została usunięta z pamięci twojego urządzenia</string>
+    <string name="all_files_removed">Wszystkie pliki zostały usunięte z Twojego urządzenia</string>
+    <string name="removing_all_files">Trwa usuwanie plików...</string>
+    <string name="no_thanks">Nie dziękuje</string>
+    <string name="no_prapremiere_title">Prapremiera</string>
+    <string name="login">Zaloguj</string>
+    <string name="downloaded_empty_list">Brak pobranych plików</string>
+    <string name="audiobooks_empty_list">Brak audiobooków</string>
+    <string name="newest_empty_list">Brak najnowszych pozycji</string>
+    <string name="recommended_empty_list">Brak rekomendowanych pozycji</string>
+    <string name="reading_empty_list">Nie czytasz jeszcze żadnej pozycji</string>
+    <string name="faviourites_empty_list">Nie masz jeszcze ulubionych pozycji</string>
+    <string name="completed_empty_list">Nie przeczytałeś jeszcze żadnej pozycji</string>
+    <string name="subscription_lost">Subskrypcja wygasła</string>
+    <string name="install_chrome">Twój system nie posiada przeglądarki. Zalecamy instalację Google Chrom i ponowienie tej próby.</string>
+    <string name="library_now_reading">Teraz czytam</string>
+    <string name="login_title">Załóż konto / Zaloguj się w Wolnych Lekturach, aby:</string>
+    <string name="login_benefits">dodawać książki i audiobooki do ulubionych,</string>
+    <string name="login_benefits_2">wrócić do czytania rozpoczętej już książki,</string>
+    <string name="login_benefits_3">zostać naszym przyjacielem i pomóc nam rozwijać Wolne Lektury.</string>
+    <string name="read_now_library_empty">Zacznij czytać już teraz!</string>
+    <string name="all_files_failed_to_remove">Nie udało się usunąc danych. Wystąpił błąd: %s</string>
+</resources>
diff --git a/Android/app/src/main/res/values/styles.xml b/Android/app/src/main/res/values/styles.xml
new file mode 100644 (file)
index 0000000..30f9693
--- /dev/null
@@ -0,0 +1,193 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+    <!-- Base application theme. -->
+    <style name="WLAppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+        <item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
+        <item name="android:windowBackground">@color/white</item>
+    </style>
+
+    <style name="WLAppTheme.NoActionBar" parent="Theme.AppCompat.Light.NoActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+        <item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
+        <item name="android:windowBackground">@color/white</item>
+    </style>
+
+    <!--use item name="drawerArrowStyle" if you are using dark primaryColor in your app, else remove it-->
+    <style name="DrawerArrowStyle" parent="Widget.AppCompat.DrawerArrowToggle">
+        <item name="spinBars">false</item>
+        <item name="color">@android:color/white</item>
+    </style>
+
+    <style name="FlatButton" parent="android:style/Widget.Material.Button.Borderless.Small">
+        <item name="android:background">@android:color/transparent</item>
+        <item name="android:textColor">@color/gray_dark</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textSize">13dp</item>
+        <item name="android:textStyle">normal</item>
+    </style>
+
+    <style name="FlatButton.Separator" parent="@style/Widget.AppCompat.Button.Borderless">
+        <item name="android:drawableRight">@drawable/ic_arrow_right_24dp</item>
+        <item name="android:textColor">@color/gray_dark</item>
+    </style>
+
+    <style name="RoundedButton" parent="android:style/Widget.Material.Button">
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textSize">14dp</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:paddingTop">5dp</item>
+        <item name="android:paddingBottom">5dp</item>
+        <item name="android:paddingStart">20dp</item>
+        <item name="android:paddingEnd">20dp</item>
+        <item name="android:layout_height">35dp</item>
+        <item name="android:layout_width">wrap_content</item>
+    </style>
+
+    <style name="RoundedButton.Orange" parent="RoundedButton">
+        <item name="android:background">@drawable/orange_round_rect</item>
+        <item name="android:textColor">@color/white</item>
+    </style>
+
+    <style name="RoundedButton.WhiteBorder" parent="RoundedButton">
+        <item name="android:background">@drawable/selector_button_white_border</item>
+        <item name="android:textColor">@color/selector_button_white_border_text_color</item>
+    </style>
+
+    <style name="OrangeDetailsButton" parent="@style/Widget.AppCompat.Button.Borderless">
+        <item name="android:background">@drawable/orange_details_round_rect</item>
+        <item name="android:textColor">@color/white</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textSize">@dimen/book_button_text_size</item>
+        <item name="android:gravity">start|center_vertical</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:paddingStart">@dimen/book_button_padding</item>
+        <item name="android:paddingEnd">@dimen/book_button_padding</item>
+        <item name="android:minHeight">0dp</item>
+        <item name="android:minWidth">0dp</item>
+    </style>
+
+    <style name="SeparatorText" parent="@android:style/TextAppearance">
+        <item name="android:textSize">14dp</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textColor">@color/gray_dark</item>
+    </style>
+
+    <style name="SeparatorText.White" parent="SeparatorText">
+        <item name="android:textSize">16dp</item>
+        <item name="android:textColor">@color/white</item>
+    </style>
+
+    <style name="BookHeaderTextView" parent="android:style/Widget.TextView">
+        <item name="android:textSize" tools:ignore="SpUsage">18dp</item>
+        <item name="android:textColor">@color/white</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:textStyle">normal</item>
+    </style>
+
+    <style name="BookHeaderDarkTextView" parent="android:style/Widget.TextView">
+        <item name="android:textSize" tools:ignore="SpUsage">18dp</item>
+        <item name="android:textColor">@color/gray_dark</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:textStyle">normal</item>
+    </style>
+
+    <style name="ListHeaderText" parent="@android:style/TextAppearance">
+        <item name="android:textSize">20dp</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textColor">@color/red_dark</item>
+    </style>
+
+    <style name="ListTitleText" parent="@android:style/TextAppearance">
+        <item name="android:textSize">15dp</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:lines">1</item>
+        <item name="android:ellipsize">end</item>
+    </style>
+
+    <style name="ListTitleText.Black">
+        <item name="android:textColor">@color/gray_dark</item>
+    </style>
+
+    <style name="ListTitleText.Turquoise">
+        <item name="android:textColor">@color/turquoise</item>
+    </style>
+
+    <style name="ListTitleText.Orange">
+        <item name="android:textColor">@color/orange_light</item>
+    </style>
+
+    <style name="ListTableTitleText" parent="@android:style/TextAppearance">
+        <item name="android:textSize">10dp</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:layout_marginEnd">5dp</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textColor">@color/gray_dark</item>
+    </style>
+
+    <style name="BookCategoriesTitleText" parent="@android:style/TextAppearance">
+        <item name="android:textSize">11dp</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textColor">@color/gray_dark</item>
+    </style>
+
+    <style name="DarkBackgroundSwitch">
+        <item name="colorControlActivated">@color/white</item>
+    </style>
+
+    <style name="NewsHeaderTextView" parent="android:style/Widget.TextView">
+        <item name="android:textSize" tools:ignore="SpUsage">20dp</item>
+        <item name="android:textColor">@color/turquoise</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textAllCaps">true</item>
+    </style>
+
+    <style name="NewsText" parent="@android:style/TextAppearance">
+        <item name="android:textSize" tools:ignore="SpUsage">15dp</item>
+    </style>
+
+    <style name="NewsText.Black">
+        <item name="android:textColor">@color/gray_dark</item>
+    </style>
+
+    <style name="NewsText.TurquoiseBold">
+        <item name="android:textColor">@color/turquoise</item>
+        <item name="android:textStyle">bold</item>
+    </style>
+
+    <style name="PlayerPlaylistText" parent="@android:style/TextAppearance">
+        <item name="android:textColor">@color/white</item>
+        <item name="android:textSize" tools:ignore="SpUsage">18dp</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+    </style>
+
+    <style name="SettingsSeparator">
+        <item name="android:layout_marginBottom">10dp</item>
+        <item name="android:layout_marginEnd">25dp</item>
+        <item name="android:layout_marginStart">25dp</item>
+        <item name="android:layout_marginTop">10dp</item>
+        <item name="android:background">@color/turquoise</item>
+    </style>
+
+    <style name="CircularProgress" parent="WLAppTheme">
+        <item name="colorAccent">@color/white</item>
+    </style>
+
+    <style name="LoginBenefitsText" parent="@android:style/TextAppearance">
+        <item name="android:textColor">@color/white</item>
+        <item name="android:textSize" tools:ignore="SpUsage">14dp</item>
+        <item name="android:paddingTop">5dp</item>
+        <item name="android:paddingBottom">5dp</item>
+        <item name="android:drawableStart">@drawable/accept_orange_tint</item>
+        <item name="android:drawablePadding">10dp</item>
+    </style>
+</resources>
diff --git a/Android/app/src/test/java/com/moiseum/wolnelektury/ExampleUnitTest.java b/Android/app/src/test/java/com/moiseum/wolnelektury/ExampleUnitTest.java
new file mode 100644 (file)
index 0000000..ed2f4a5
--- /dev/null
@@ -0,0 +1,17 @@
+package com.moiseum.wolnelektury;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+       @Test
+       public void addition_isCorrect() throws Exception {
+               assertEquals(4, 2 + 2);
+       }
+}
\ No newline at end of file
diff --git a/Android/build.gradle b/Android/build.gradle
new file mode 100644 (file)
index 0000000..93fa7d7
--- /dev/null
@@ -0,0 +1,44 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    ext.objectboxVersion = '1.4.4'
+
+    repositories {
+        google()
+        jcenter()
+        maven {
+            url "http://dl.bintray.com/mobisystech/maven"
+        }
+        maven {
+            url "http://objectbox.net/beta-repo/"
+        }
+        maven {
+            url 'https://maven.fabric.io/public'
+        }
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.1.2'
+        classpath 'com.google.gms:google-services:4.0.1'
+        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
+        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
+        classpath 'io.fabric.tools:gradle:1.25.4'
+        classpath "io.objectbox:objectbox-gradle-plugin:$objectboxVersion"
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+        maven {
+            url "http://dl.bintray.com/mobisystech/maven"
+        }
+        maven {
+            url "http://objectbox.net/beta-repo/"
+        }
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/Android/config/quality/checkstyle/checkstyle-config.xml b/Android/config/quality/checkstyle/checkstyle-config.xml
new file mode 100755 (executable)
index 0000000..262a6c3
--- /dev/null
@@ -0,0 +1,167 @@
+<?xml version="1.0"?>\r
+<!DOCTYPE module PUBLIC\r
+    "-//Puppy Crawl//DTD Check Configuration 1.3//EN"\r
+    "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">\r
+\r
+<module name = "Checker">\r
+\r
+    <property name="charset" value="UTF-8"/>\r
+\r
+    <property name="severity" value="error"/>\r
+\r
+    <module name="FileTabCharacter">\r
+        <property name="eachLine" value="true"/>\r
+    </module>\r
+\r
+    <module name="TreeWalker">\r
+\r
+        <!-- Imports -->\r
+\r
+        <module name="RedundantImport">\r
+            <property name="severity" value="error"/>\r
+        </module>\r
+\r
+        <module name="AvoidStarImport">\r
+            <property name="severity" value="error"/>\r
+        </module>\r
+\r
+        <!-- General Code Style -->\r
+\r
+        <module name="LineLength">\r
+            <property name="max" value="100"/>\r
+            <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://|^* static"/>\r
+        </module>\r
+\r
+        <module name="EmptyBlock">\r
+            <property name="option" value="TEXT"/>\r
+            <property name="tokens" value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>\r
+        </module>\r
+\r
+        <module name="EmptyCatchBlock">\r
+            <property name="exceptionVariableName" value="expected"/>\r
+        </module>\r
+\r
+        <module name="LeftCurly">\r
+            <property name="maxLineLength" value="100"/>\r
+        </module>\r
+\r
+        <module name="RightCurly">\r
+            <property name="option" value="alone"/>\r
+            <property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO, STATIC_INIT, INSTANCE_INIT"/>\r
+        </module>\r
+\r
+        <module name="RightCurly">\r
+            <property name="option" value="same"/>\r
+        </module>\r
+\r
+        <module name="NoFinalizer"/>\r
+\r
+        <module name="ArrayTypeStyle"/>\r
+\r
+        <module name="ModifierOrder"/>\r
+\r
+        <module name="Indentation">\r
+            <property name="basicOffset" value="4"/>\r
+            <property name="braceAdjustment" value="0"/>\r
+            <property name="caseIndent" value="4"/>\r
+            <property name="throwsIndent" value="4"/>\r
+            <property name="lineWrappingIndentation" value="8"/>\r
+            <property name="arrayInitIndent" value="2"/>\r
+        </module>\r
+\r
+        <!-- White Space -->\r
+\r
+        <module name="GenericWhitespace">\r
+            <message key="ws.followed"\r
+                     value="GenericWhitespace ''{0}'' is followed by whitespace."/>\r
+            <message key="ws.preceded"\r
+                     value="GenericWhitespace ''{0}'' is preceded with whitespace."/>\r
+            <message key="ws.illegalFollow"\r
+                     value="GenericWhitespace ''{0}'' should followed by whitespace."/>\r
+            <message key="ws.notPreceded"\r
+                     value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>\r
+        </module>\r
+\r
+        <module name="WhitespaceAround">\r
+            <property name="allowEmptyConstructors" value="true"/>\r
+            <property name="allowEmptyMethods" value="false"/>\r
+            <property name="allowEmptyTypes" value="false"/>\r
+            <property name="allowEmptyLoops" value="false"/>\r
+            <message key="ws.notFollowed"\r
+                     value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>\r
+            <message key="ws.notPreceded"\r
+                     value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>\r
+            <property name="severity" value="error"/>\r
+        </module>\r
+\r
+        <module name="WhitespaceAfter">\r
+            <property name="tokens" value="COMMA, SEMI, TYPECAST"/>\r
+        </module>\r
+\r
+        <module name="NoWhitespaceBefore">\r
+            <property name="tokens" value="SEMI, DOT, POST_DEC, POST_INC"/>\r
+            <property name="allowLineBreaks" value="true"/>\r
+        </module>\r
+\r
+        <module name="NoWhitespaceAfter">\r
+            <property name="tokens" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS, UNARY_PLUS"/>\r
+            <property name="allowLineBreaks" value="true"/>\r
+        </module>\r
+\r
+        <!-- Naming -->\r
+\r
+        <module name="PackageName">\r
+            <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Package name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="MethodName">\r
+            <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Method name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="TypeName">\r
+            <message key="name.invalidPattern"\r
+                     value="Type name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="MemberName">\r
+            <property name="applyToPublic" value="false" />\r
+            <property name="applyToPackage" value="false" />\r
+            <property name="applyToProtected" value="false" />\r
+            <property name="format" value="^m[A-Z]+[a-z0-9][a-zA-Z0-9]*$"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Member name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="ParameterName">\r
+            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Parameter name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="LocalVariableName">\r
+            <property name="tokens" value="VARIABLE_DEF"/>\r
+            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>\r
+            <property name="allowOneCharVarInForLoop" value="true"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Local variable name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="ClassTypeParameterName">\r
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Class type name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="MethodTypeParameterName">\r
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Method type name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+    </module>\r
+\r
+</module>
\ No newline at end of file
diff --git a/Android/config/quality/findbugs/android-exclude-filter.xml b/Android/config/quality/findbugs/android-exclude-filter.xml
new file mode 100755 (executable)
index 0000000..b724212
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<FindBugsFilter>
+    <Match>
+        <Class name="~.*\.R\$.*"/>
+    </Match>
+    <Match>
+        <Class name="~.*\.Manifest\$.*"/>
+    </Match>
+    <!-- All bugs in test classes, except for JUnit-specific bugs -->
+    <Match>
+        <Class name="~.*\.*Test" />
+        <Not>
+            <Bug code="IJU" />
+        </Not>
+    </Match>
+
+
+</FindBugsFilter>
\ No newline at end of file
diff --git a/Android/config/quality/pmd/pmd-ruleset.xml b/Android/config/quality/pmd/pmd-ruleset.xml
new file mode 100755 (executable)
index 0000000..ad41893
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Android Application Rules"
+    xmlns="http://pmd.sf.net/ruleset/1.0.0"
+    xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"
+    xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd">
+
+    <description>Custom ruleset for ribot Android application</description>
+
+    <exclude-pattern>.*/R.java</exclude-pattern>
+    <exclude-pattern>.*/gen/.*</exclude-pattern>
+
+    <rule ref="rulesets/java/android.xml" />
+    <rule ref="rulesets/java/clone.xml" />
+    <rule ref="rulesets/java/finalizers.xml" />
+    <rule ref="rulesets/java/imports.xml">
+        <!-- Espresso is designed this way !-->
+        <exclude name="TooManyStaticImports" />
+    </rule>
+    <rule ref="rulesets/java/logging-java.xml">
+        <!-- This rule wasn't working properly and given errors in every var call info -->
+        <exclude name="GuardLogStatementJavaUtil" />
+    </rule>
+    <rule ref="rulesets/java/braces.xml">
+        <!-- We allow single line if's without braces -->
+        <exclude name="IfStmtsMustUseBraces" />
+    </rule>
+    <rule ref="rulesets/java/strings.xml" />
+    <rule ref="rulesets/java/basic.xml" />
+    <rule ref="rulesets/java/naming.xml">
+        <exclude name="AbstractNaming" />
+        <exclude name="LongVariable" />
+        <exclude name="ShortMethodName" />
+        <exclude name="ShortVariable" />
+        <exclude name="ShortClassName" />
+        <exclude name="VariableNamingConventions" />
+    </rule>
+</ruleset>
\ No newline at end of file
diff --git a/Android/config/quality/quality.gradle b/Android/config/quality/quality.gradle
new file mode 100755 (executable)
index 0000000..8a06897
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Set up Checkstyle, Findbugs and PMD to perform extensive code analysis.
+ *
+ * Gradle tasks added:
+ * - checkstyle
+ * - findbugs
+ * - pmd
+ *
+ * The three tasks above are added as dependencies of the check task so running check will
+ * run all of them.
+ */
+
+apply plugin: 'checkstyle'
+apply plugin: 'findbugs'
+apply plugin: 'pmd'
+
+dependencies {
+    checkstyle 'com.puppycrawl.tools:checkstyle:6.5'
+}
+
+def qualityConfigDir = "$project.rootDir/config/quality"
+def reportsDir = "$project.buildDir/reports"
+
+check.dependsOn 'checkstyle', 'findbugs', 'pmd'
+
+task checkstyle(type: Checkstyle, group: 'Verification', description: 'Runs code style checks') {
+    configFile file("$qualityConfigDir/checkstyle/checkstyle-config.xml")
+    source 'src'
+    include '**/*.java'
+
+    reports {
+        xml.enabled = true
+        xml {
+            destination "$reportsDir/checkstyle/checkstyle.xml"
+        }
+    }
+
+    classpath = files( )
+}
+
+task findbugs(type: FindBugs,
+        group: 'Verification',
+        description: 'Inspect java bytecode for bugs',
+        dependsOn: ['compileDebugSources','compileReleaseSources']) {
+
+    ignoreFailures = false
+    effort = "max"
+    reportLevel = "high"
+    excludeFilter = new File("$qualityConfigDir/findbugs/android-exclude-filter.xml")
+    classes = files("$project.rootDir/folioreader/build/intermediates/classes")
+
+    source 'src'
+    include '**/*.java'
+    exclude '**/gen/**'
+
+    reports {
+        xml.enabled = false
+        html.enabled = true
+        xml {
+            destination "$reportsDir/findbugs/findbugs.xml"
+        }
+        html {
+            destination "$reportsDir/findbugs/findbugs.html"
+        }
+    }
+
+    classpath = files()
+}
+
+
+task pmd(type: Pmd, group: 'Verification', description: 'Inspect sourcecode for bugs') {
+    ruleSetFiles = files("$qualityConfigDir/pmd/pmd-ruleset.xml")
+    ignoreFailures = false
+    ruleSets = []
+
+    source 'src'
+    include '**/*.java'
+    exclude '**/gen/**'
+
+    reports {
+        xml.enabled = true
+        html.enabled = true
+        xml {
+            destination "$reportsDir/pmd/pmd.xml"
+        }
+        html {
+            destination "$reportsDir/pmd/pmd.html"
+        }
+    }
+}
\ No newline at end of file
diff --git a/Android/folioreader/AndroidManifest.xml b/Android/folioreader/AndroidManifest.xml
new file mode 100755 (executable)
index 0000000..d62406c
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"\r
+    package="com.folioreader">\r
+\r
+    <application>\r
+        <activity android:name=".ui.folio.activity.ContentHighlightActivity" />\r
+    </application>\r
+\r
+</manifest>
\ No newline at end of file
diff --git a/Android/folioreader/bintray/bintrayv1.gradle b/Android/folioreader/bintray/bintrayv1.gradle
new file mode 100755 (executable)
index 0000000..fa392e4
--- /dev/null
@@ -0,0 +1,64 @@
+apply plugin: 'com.jfrog.bintray'
+
+version = libraryVersion
+
+if (project.hasProperty("android")) { // Android libraries
+    task sourcesJar(type: Jar) {
+        classifier = 'sources'
+        from android.sourceSets.main.java.srcDirs
+    }
+
+//    task javadoc(type: Javadoc) {
+//        source = android.sourceSets.main.java.srcDirs
+//        classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+//    }
+} else { // Java libraries
+    task sourcesJar(type: Jar, dependsOn: classes) {
+        classifier = 'sources'
+        from sourceSets.main.allSource
+    }
+}
+
+//task javadocJar(type: Jar, dependsOn: javadoc) {
+//    classifier = 'javadoc'
+//    from javadoc.destinationDir
+//}
+
+artifacts {
+    //archives javadocJar
+    archives sourcesJar
+}
+
+// Bintray
+def propertiesFile = project.rootProject.file('local.properties')
+if( propertiesFile.exists()) {
+    Properties properties = new Properties()
+    properties.load(propertiesFile.newDataInputStream())
+
+    bintray {
+        user = properties.getProperty("bintray.user")
+        key = properties.getProperty("bintray.apikey")
+
+        configurations = ['archives']
+        pkg {
+            repo = bintrayRepo
+            name = bintrayName
+            desc = libraryDescription
+            websiteUrl = siteUrl
+            vcsUrl = gitUrl
+            licenses = allLicenses
+            publish = true
+            publicDownloadNumbers = true
+            version {
+                desc = libraryDescription
+                gpg {
+                    sign = true //Determines whether to GPG sign the files. The default is false
+                    passphrase = properties.getProperty("bintray.gpg.password")
+                    //Optional. The passphrase for GPG signing'
+                }
+            }
+        }
+    }
+} else {
+    logger.info("local.properties does not exist. Skipping Bintray.")
+}
\ No newline at end of file
diff --git a/Android/folioreader/bintray/installv1.gradle b/Android/folioreader/bintray/installv1.gradle
new file mode 100755 (executable)
index 0000000..0396115
--- /dev/null
@@ -0,0 +1,42 @@
+apply plugin: 'com.github.dcendents.android-maven'
+
+group = publishedGroupId                               // Maven Group ID for the artifact
+
+install {
+    repositories.mavenInstaller {
+        // This generates POM.xml with proper parameters
+        pom {
+            project {
+                packaging 'aar'
+                groupId publishedGroupId
+                artifactId artifact
+
+                // Add your description here
+                name libraryName
+                description libraryDescription
+                url siteUrl
+
+                // Set your license
+                licenses {
+                    license {
+                        name licenseName
+                        url licenseUrl
+                    }
+                }
+                developers {
+                    developer {
+                        id developerId
+                        name developerName
+                        email developerEmail
+                    }
+                }
+                scm {
+                    connection gitUrl
+                    developerConnection gitUrl
+                    url siteUrl
+
+                }
+            }
+        }
+    }
+}
diff --git a/Android/folioreader/build.gradle b/Android/folioreader/build.gradle
new file mode 100755 (executable)
index 0000000..74244bf
--- /dev/null
@@ -0,0 +1,103 @@
+apply plugin: 'com.android.library'
+apply from: '../config/quality/quality.gradle'
+apply plugin: 'com.github.dcendents.android-maven'
+
+ext {
+    bintrayRepo = 'maven'
+    bintrayName = 'folioreader'
+
+    publishedGroupId = 'com.folioreader'
+    libraryName = 'FolioReader'
+    artifact = 'folioreader'
+
+    libraryDescription = 'An epub reader for Android'
+
+    siteUrl = 'https://github.com/FolioReader/FolioReader-Android'
+    gitUrl = 'https://github.com/FolioReader/FolioReader-Android.git'
+
+    libraryVersion = '0.3.1'
+
+    developerId = 'mobisystech'
+    developerName = 'Folio Reader'
+    developerEmail = 'mahavir@codetoart.com'
+
+    licenseName = 'FreeBSD License'
+    licenseUrl = 'https://en.wikipedia.org/wiki/FreeBSD_Documentation_License#License'
+    allLicenses = ["FreeBSD"]
+}
+
+android {
+    useLibrary 'org.apache.http.legacy'
+    compileSdkVersion 27
+    buildToolsVersion '27.0.3'
+
+    defaultConfig {
+        versionCode 1
+        versionName "1.0"
+        minSdkVersion 19
+        targetSdkVersion 26
+    }
+
+    sourceSets {
+        main {
+            manifest.srcFile 'AndroidManifest.xml'
+            java.srcDirs = ['src/main/java']
+            res.srcDirs = ['res']
+        }
+        test {
+            java.srcDirs = ['src/test/java']
+        }
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+
+    packagingOptions {
+        exclude 'META-INF/ASL2.0'
+        exclude 'META-INF/DEPENDENCIES.txt'
+        exclude 'META-INF/LICENSE.txt'
+        exclude 'META-INF/NOTICE.txt'
+        exclude 'META-INF/NOTICE'
+        exclude 'META-INF/LICENSE'
+        exclude 'META-INF/DEPENDENCIES'
+        exclude 'META-INF/notice.txt'
+        exclude 'META-INF/license.txt'
+        exclude 'META-INF/dependencies.txt'
+        exclude 'META-INF/LGPL2.1'
+        exclude 'META-INF/services/javax.annotation.processing.Processor'
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    checkstyle {
+        ignoreFailures = true
+    }
+}
+
+dependencies {
+    compile fileTree(include: ['*.jar'], dir: 'libs')
+    compile project(':webViewMarker')
+    compile project(':r2-streamer:r2-fetcher')
+    compile project(':r2-streamer:r2-parser')
+    compile project(':r2-streamer:r2-server')
+
+    final ANDROID_LIB_VERSION = '27.0.0'
+
+    //noinspection GradleDependency
+    compile "com.android.support:appcompat-v7:$ANDROID_LIB_VERSION"
+    compile "com.android.support:recyclerview-v7:$ANDROID_LIB_VERSION"
+    compile "com.android.support:support-v4:$ANDROID_LIB_VERSION"
+    compile "com.android.support:design:$ANDROID_LIB_VERSION"
+
+
+    compile 'com.daimajia.swipelayout:library:1.2.0@aar'
+
+    compile 'com.squareup:otto:1.3.8'
+}
+
+apply from: '../folioreader/bintray/installv1.gradle'
+apply from: '../folioreader/bintray/bintrayv1.gradle'
diff --git a/Android/folioreader/libs/epublib-core-latest.jar b/Android/folioreader/libs/epublib-core-latest.jar
new file mode 100755 (executable)
index 0000000..6799a30
Binary files /dev/null and b/Android/folioreader/libs/epublib-core-latest.jar differ
diff --git a/Android/folioreader/libs/slf4j-android-1.5.8.jar b/Android/folioreader/libs/slf4j-android-1.5.8.jar
new file mode 100755 (executable)
index 0000000..e0128bc
Binary files /dev/null and b/Android/folioreader/libs/slf4j-android-1.5.8.jar differ
diff --git a/Android/folioreader/res/anim/disappear.xml b/Android/folioreader/res/anim/disappear.xml
new file mode 100755 (executable)
index 0000000..21e6a4d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <alpha
+            android:interpolator="@android:anim/decelerate_interpolator"
+            android:fromAlpha="1.0" android:toAlpha="0.0"
+            android:duration="400"
+    />
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/enter_from_left.xml b/Android/folioreader/res/anim/enter_from_left.xml
new file mode 100755 (executable)
index 0000000..7e0506f
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shareInterpolator="false">
+    <translate
+        android:fromXDelta="-100%" android:toXDelta="0%"
+        android:fromYDelta="0%" android:toYDelta="0%"
+        android:duration="700"/>
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/enter_from_right.xml b/Android/folioreader/res/anim/enter_from_right.xml
new file mode 100755 (executable)
index 0000000..a78db39
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shareInterpolator="false">
+    <translate
+        android:fromXDelta="100%" android:toXDelta="0%"
+        android:fromYDelta="0%" android:toYDelta="0%"
+        android:duration="700" />
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/exit_to_left.xml b/Android/folioreader/res/anim/exit_to_left.xml
new file mode 100755 (executable)
index 0000000..8c07673
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shareInterpolator="false">
+    <translate
+        android:fromXDelta="0%" android:toXDelta="-100%"
+        android:fromYDelta="0%" android:toYDelta="0%"
+        android:duration="700"/>
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/exit_to_right.xml b/Android/folioreader/res/anim/exit_to_right.xml
new file mode 100755 (executable)
index 0000000..255d47a
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shareInterpolator="false">
+    <translate
+        android:fromXDelta="0%" android:toXDelta="100%"
+        android:fromYDelta="0%" android:toYDelta="0%"
+        android:duration="700" />
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/fadein.xml b/Android/folioreader/res/anim/fadein.xml
new file mode 100755 (executable)
index 0000000..3c7c297
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:interpolator="@android:anim/linear_interpolator">
+    <alpha
+            android:fromAlpha="0.1"
+            android:toAlpha="1.0"
+            android:duration="1500"
+    />
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/fadeout.xml b/Android/folioreader/res/anim/fadeout.xml
new file mode 100755 (executable)
index 0000000..836015d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:interpolator="@android:anim/linear_interpolator">
+    <alpha
+            android:fromAlpha="1.0"
+            android:toAlpha="0.1"
+            android:duration="1500"
+    />
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/grow_from_bottom.xml b/Android/folioreader/res/anim/grow_from_bottom.xml
new file mode 100755 (executable)
index 0000000..b9edc5a
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="0.3" android:toXScale="1.0"
+            android:fromYScale="0.3" android:toYScale="1.0"
+            android:pivotX="50%" android:pivotY="100%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/decelerate_interpolator"
+            android:fromAlpha="0.0" android:toAlpha="1.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
diff --git a/Android/folioreader/res/anim/grow_from_bottomleft_to_topright.xml b/Android/folioreader/res/anim/grow_from_bottomleft_to_topright.xml
new file mode 100755 (executable)
index 0000000..7cbecad
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="0.3" android:toXScale="1.0"
+            android:fromYScale="0.3" android:toYScale="1.0"
+            android:pivotX="0%" android:pivotY="50%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/decelerate_interpolator"
+            android:fromAlpha="0.0" android:toAlpha="1.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/grow_from_bottomright_to_topleft.xml b/Android/folioreader/res/anim/grow_from_bottomright_to_topleft.xml
new file mode 100755 (executable)
index 0000000..965f090
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<set xmlns:android="http://schemas.android.com/apk/res/android">\r
+    <scale\r
+            android:fromXScale="0.3" android:toXScale="1.0"\r
+            android:fromYScale="0.3" android:toYScale="1.0"\r
+            android:pivotX="100%" android:pivotY="50%"\r
+            android:duration="@android:integer/config_shortAnimTime"\r
+    />\r
+    <alpha\r
+            android:interpolator="@android:anim/decelerate_interpolator"\r
+            android:fromAlpha="0.0" android:toAlpha="1.0"\r
+            android:duration="@android:integer/config_shortAnimTime"\r
+    />\r
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/grow_from_top.xml b/Android/folioreader/res/anim/grow_from_top.xml
new file mode 100755 (executable)
index 0000000..e2831ff
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="0.3" android:toXScale="1.0"
+            android:fromYScale="0.3" android:toYScale="1.0"
+            android:pivotX="50%" android:pivotY="0%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/decelerate_interpolator"
+            android:fromAlpha="0.0" android:toAlpha="1.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
diff --git a/Android/folioreader/res/anim/grow_from_topleft_to_bottomright.xml b/Android/folioreader/res/anim/grow_from_topleft_to_bottomright.xml
new file mode 100755 (executable)
index 0000000..a75e86c
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="0.3" android:toXScale="1.0"
+            android:fromYScale="0.3" android:toYScale="1.0"
+            android:pivotX="0%" android:pivotY="0%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/decelerate_interpolator"
+            android:fromAlpha="0.0" android:toAlpha="1.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
diff --git a/Android/folioreader/res/anim/grow_from_topright_to_bottomleft.xml b/Android/folioreader/res/anim/grow_from_topright_to_bottomleft.xml
new file mode 100755 (executable)
index 0000000..141c19c
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<set xmlns:android="http://schemas.android.com/apk/res/android">\r
+    <scale\r
+            android:fromXScale="0.3" android:toXScale="1.0"\r
+            android:fromYScale="0.3" android:toYScale="1.0"\r
+            android:pivotX="100%" android:pivotY="0%"\r
+            android:duration="@android:integer/config_shortAnimTime"\r
+    />\r
+    <alpha\r
+            android:interpolator="@android:anim/decelerate_interpolator"\r
+            android:fromAlpha="0.0" android:toAlpha="1.0"\r
+            android:duration="@android:integer/config_shortAnimTime"\r
+    />\r
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/pump_bottom.xml b/Android/folioreader/res/anim/pump_bottom.xml
new file mode 100755 (executable)
index 0000000..2c8ec4c
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="1.1" android:toXScale="1.0"
+            android:fromYScale="1.1" android:toYScale="1.0"
+            android:pivotX="50%" android:pivotY="100%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/decelerate_interpolator"
+            android:fromAlpha="0.0" android:toAlpha="1.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
diff --git a/Android/folioreader/res/anim/pump_top.xml b/Android/folioreader/res/anim/pump_top.xml
new file mode 100755 (executable)
index 0000000..c77e8cf
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="1.1" android:toXScale="1.0"
+            android:fromYScale="1.1" android:toYScale="1.0"
+            android:pivotX="50%" android:pivotY="0%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/decelerate_interpolator"
+            android:fromAlpha="0.0" android:toAlpha="1.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
diff --git a/Android/folioreader/res/anim/shrink_from_bottom.xml b/Android/folioreader/res/anim/shrink_from_bottom.xml
new file mode 100755 (executable)
index 0000000..98a15c9
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="1.0" android:toXScale="0.3"
+            android:fromYScale="1.0" android:toYScale="0.3"
+            android:pivotX="50%" android:pivotY="0%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/accelerate_interpolator"
+            android:fromAlpha="1.0" android:toAlpha="0.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
diff --git a/Android/folioreader/res/anim/shrink_from_bottomleft_to_topright.xml b/Android/folioreader/res/anim/shrink_from_bottomleft_to_topright.xml
new file mode 100755 (executable)
index 0000000..0551a76
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<set xmlns:android="http://schemas.android.com/apk/res/android">\r
+    <scale\r
+            android:fromXScale="1.0" android:toXScale="0.3"\r
+            android:fromYScale="1.0" android:toYScale="0.3"\r
+            android:pivotX="100%" android:pivotY="0%"\r
+            android:duration="@android:integer/config_shortAnimTime"\r
+    />\r
+    <alpha\r
+            android:interpolator="@android:anim/accelerate_interpolator"\r
+            android:fromAlpha="1.0" android:toAlpha="0.0"\r
+            android:duration="@android:integer/config_shortAnimTime"\r
+    />\r
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/shrink_from_bottomright_to_topleft.xml b/Android/folioreader/res/anim/shrink_from_bottomright_to_topleft.xml
new file mode 100755 (executable)
index 0000000..bcbb9a9
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="1.0" android:toXScale="0.3"
+            android:fromYScale="1.0" android:toYScale="0.3"
+            android:pivotX="0%" android:pivotY="0%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/accelerate_interpolator"
+            android:fromAlpha="1.0" android:toAlpha="0.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
diff --git a/Android/folioreader/res/anim/shrink_from_top.xml b/Android/folioreader/res/anim/shrink_from_top.xml
new file mode 100755 (executable)
index 0000000..8ea8ab7
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="1.0" android:toXScale="0.3"
+            android:fromYScale="1.0" android:toYScale="0.3"
+            android:pivotX="50%" android:pivotY="100%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/accelerate_interpolator"
+            android:fromAlpha="1.0" android:toAlpha="0.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
diff --git a/Android/folioreader/res/anim/shrink_from_topleft_to_bottomright.xml b/Android/folioreader/res/anim/shrink_from_topleft_to_bottomright.xml
new file mode 100755 (executable)
index 0000000..86cb79f
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<set xmlns:android="http://schemas.android.com/apk/res/android">\r
+    <scale\r
+            android:fromXScale="1.0" android:toXScale="0.3"\r
+            android:fromYScale="1.0" android:toYScale="0.3"\r
+            android:pivotX="100%" android:pivotY="100%"\r
+            android:duration="@android:integer/config_shortAnimTime"\r
+    />\r
+    <alpha\r
+            android:interpolator="@android:anim/accelerate_interpolator"\r
+            android:fromAlpha="1.0" android:toAlpha="0.0"\r
+            android:duration="@android:integer/config_shortAnimTime"\r
+    />\r
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/shrink_from_topright_to_bottomleft.xml b/Android/folioreader/res/anim/shrink_from_topright_to_bottomleft.xml
new file mode 100755 (executable)
index 0000000..11103f6
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <scale
+            android:fromXScale="1.0" android:toXScale="0.3"
+            android:fromYScale="1.0" android:toYScale="0.3"
+            android:pivotX="0%" android:pivotY="100%"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+    <alpha
+            android:interpolator="@android:anim/accelerate_interpolator"
+            android:fromAlpha="1.0" android:toAlpha="0.0"
+            android:duration="@android:integer/config_shortAnimTime"
+    />
+</set>
diff --git a/Android/folioreader/res/anim/slide_down.xml b/Android/folioreader/res/anim/slide_down.xml
new file mode 100755 (executable)
index 0000000..f7d6124
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android" >
+    <translate
+        android:duration="300"
+        android:fromYDelta="0"
+        android:toYDelta="100%" />
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/slide_in_up.xml b/Android/folioreader/res/anim/slide_in_up.xml
new file mode 100755 (executable)
index 0000000..1755d2c
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="700"
+    android:fromYDelta="100%p"
+    android:toYDelta="0%p" />
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/slide_out_up.xml b/Android/folioreader/res/anim/slide_out_up.xml
new file mode 100755 (executable)
index 0000000..6d346f8
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="700"
+    android:fromYDelta="0%p"
+    android:toYDelta="-100%p" />
\ No newline at end of file
diff --git a/Android/folioreader/res/anim/slide_up.xml b/Android/folioreader/res/anim/slide_up.xml
new file mode 100755 (executable)
index 0000000..ed6a986
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <translate
+        android:duration="300"
+        android:fromYDelta="100%"
+        android:toYDelta="0" />
+</set>
\ No newline at end of file
diff --git a/Android/folioreader/res/color/content_highlight_text_selector_night_mode.xml b/Android/folioreader/res/color/content_highlight_text_selector_night_mode.xml
new file mode 100755 (executable)
index 0000000..da04ba6
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true" android:color="@color/black"/>
+    <item android:state_focused="true" android:color="@color/black"/>
+    <item android:state_pressed="true" android:color="@color/black"/>
+    <item android:color="@color/app_green"/>
+</selector>
diff --git a/Android/folioreader/res/drawable-hdpi/font_big.png b/Android/folioreader/res/drawable-hdpi/font_big.png
new file mode 100644 (file)
index 0000000..20ef47e
Binary files /dev/null and b/Android/folioreader/res/drawable-hdpi/font_big.png differ
diff --git a/Android/folioreader/res/drawable-hdpi/font_small.png b/Android/folioreader/res/drawable-hdpi/font_small.png
new file mode 100644 (file)
index 0000000..08963be
Binary files /dev/null and b/Android/folioreader/res/drawable-hdpi/font_small.png differ
diff --git a/Android/folioreader/res/drawable-hdpi/ic_comment.png b/Android/folioreader/res/drawable-hdpi/ic_comment.png
new file mode 100644 (file)
index 0000000..25a86ea
Binary files /dev/null and b/Android/folioreader/res/drawable-hdpi/ic_comment.png differ
diff --git a/Android/folioreader/res/drawable-hdpi/ic_menu_all.png b/Android/folioreader/res/drawable-hdpi/ic_menu_all.png
new file mode 100644 (file)
index 0000000..cb8c33e
Binary files /dev/null and b/Android/folioreader/res/drawable-hdpi/ic_menu_all.png differ
diff --git a/Android/folioreader/res/drawable-hdpi/icon_font.png b/Android/folioreader/res/drawable-hdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..475b728
Binary files /dev/null and b/Android/folioreader/res/drawable-hdpi/icon_font.png differ
diff --git a/Android/folioreader/res/drawable-hdpi/inset_big.png b/Android/folioreader/res/drawable-hdpi/inset_big.png
new file mode 100644 (file)
index 0000000..21a871f
Binary files /dev/null and b/Android/folioreader/res/drawable-hdpi/inset_big.png differ
diff --git a/Android/folioreader/res/drawable-hdpi/inset_small.png b/Android/folioreader/res/drawable-hdpi/inset_small.png
new file mode 100644 (file)
index 0000000..47ce95d
Binary files /dev/null and b/Android/folioreader/res/drawable-hdpi/inset_small.png differ
diff --git a/Android/folioreader/res/drawable-hdpi/margin_big.png b/Android/folioreader/res/drawable-hdpi/margin_big.png
new file mode 100644 (file)
index 0000000..2911837
Binary files /dev/null and b/Android/folioreader/res/drawable-hdpi/margin_big.png differ
diff --git a/Android/folioreader/res/drawable-hdpi/margin_small.png b/Android/folioreader/res/drawable-hdpi/margin_small.png
new file mode 100644 (file)
index 0000000..af90143
Binary files /dev/null and b/Android/folioreader/res/drawable-hdpi/margin_small.png differ
diff --git a/Android/folioreader/res/drawable-mdpi/font_big.png b/Android/folioreader/res/drawable-mdpi/font_big.png
new file mode 100644 (file)
index 0000000..193a351
Binary files /dev/null and b/Android/folioreader/res/drawable-mdpi/font_big.png differ
diff --git a/Android/folioreader/res/drawable-mdpi/font_small.png b/Android/folioreader/res/drawable-mdpi/font_small.png
new file mode 100644 (file)
index 0000000..17cf800
Binary files /dev/null and b/Android/folioreader/res/drawable-mdpi/font_small.png differ
diff --git a/Android/folioreader/res/drawable-mdpi/ic_comment.png b/Android/folioreader/res/drawable-mdpi/ic_comment.png
new file mode 100644 (file)
index 0000000..6db715f
Binary files /dev/null and b/Android/folioreader/res/drawable-mdpi/ic_comment.png differ
diff --git a/Android/folioreader/res/drawable-mdpi/ic_menu_all.png b/Android/folioreader/res/drawable-mdpi/ic_menu_all.png
new file mode 100644 (file)
index 0000000..cb687a8
Binary files /dev/null and b/Android/folioreader/res/drawable-mdpi/ic_menu_all.png differ
diff --git a/Android/folioreader/res/drawable-mdpi/icon_font.png b/Android/folioreader/res/drawable-mdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..0c812cd
Binary files /dev/null and b/Android/folioreader/res/drawable-mdpi/icon_font.png differ
diff --git a/Android/folioreader/res/drawable-mdpi/inset_big.png b/Android/folioreader/res/drawable-mdpi/inset_big.png
new file mode 100644 (file)
index 0000000..322359e
Binary files /dev/null and b/Android/folioreader/res/drawable-mdpi/inset_big.png differ
diff --git a/Android/folioreader/res/drawable-mdpi/inset_small.png b/Android/folioreader/res/drawable-mdpi/inset_small.png
new file mode 100644 (file)
index 0000000..11c6365
Binary files /dev/null and b/Android/folioreader/res/drawable-mdpi/inset_small.png differ
diff --git a/Android/folioreader/res/drawable-mdpi/margin_big.png b/Android/folioreader/res/drawable-mdpi/margin_big.png
new file mode 100644 (file)
index 0000000..9031e7c
Binary files /dev/null and b/Android/folioreader/res/drawable-mdpi/margin_big.png differ
diff --git a/Android/folioreader/res/drawable-mdpi/margin_small.png b/Android/folioreader/res/drawable-mdpi/margin_small.png
new file mode 100644 (file)
index 0000000..82e50d4
Binary files /dev/null and b/Android/folioreader/res/drawable-mdpi/margin_small.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/colors_marker.png b/Android/folioreader/res/drawable-xhdpi/colors_marker.png
new file mode 100755 (executable)
index 0000000..ecf58d4
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/colors_marker.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/edit_note.png b/Android/folioreader/res/drawable-xhdpi/edit_note.png
new file mode 100755 (executable)
index 0000000..a031b9b
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/edit_note.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/font_big.png b/Android/folioreader/res/drawable-xhdpi/font_big.png
new file mode 100644 (file)
index 0000000..73c7c65
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/font_big.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/font_small.png b/Android/folioreader/res/drawable-xhdpi/font_small.png
new file mode 100644 (file)
index 0000000..5da64fb
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/font_small.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_action_discard.png b/Android/folioreader/res/drawable-xhdpi/ic_action_discard.png
new file mode 100755 (executable)
index 0000000..9eeeed1
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_action_discard.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_action_share.png b/Android/folioreader/res/drawable-xhdpi/ic_action_share.png
new file mode 100755 (executable)
index 0000000..40771e4
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_action_share.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_blue_marker.png b/Android/folioreader/res/drawable-xhdpi/ic_blue_marker.png
new file mode 100755 (executable)
index 0000000..0a9e0af
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_blue_marker.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_comment.png b/Android/folioreader/res/drawable-xhdpi/ic_comment.png
new file mode 100644 (file)
index 0000000..d2a60c0
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_comment.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_drawer.png b/Android/folioreader/res/drawable-xhdpi/ic_drawer.png
new file mode 100755 (executable)
index 0000000..2da331e
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_drawer.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_green_marker.png b/Android/folioreader/res/drawable-xhdpi/ic_green_marker.png
new file mode 100755 (executable)
index 0000000..2f47b52
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_green_marker.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_menu_all.png b/Android/folioreader/res/drawable-xhdpi/ic_menu_all.png
new file mode 100644 (file)
index 0000000..3cf4ac0
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_menu_all.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_pink_marker.png b/Android/folioreader/res/drawable-xhdpi/ic_pink_marker.png
new file mode 100755 (executable)
index 0000000..4834604
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_pink_marker.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_underline_marker.png b/Android/folioreader/res/drawable-xhdpi/ic_underline_marker.png
new file mode 100755 (executable)
index 0000000..601a06f
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_underline_marker.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/ic_yellow_marker.png b/Android/folioreader/res/drawable-xhdpi/ic_yellow_marker.png
new file mode 100755 (executable)
index 0000000..de18c71
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/ic_yellow_marker.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/icon_close.png b/Android/folioreader/res/drawable-xhdpi/icon_close.png
new file mode 100755 (executable)
index 0000000..365f537
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/icon_close.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/icon_font.png b/Android/folioreader/res/drawable-xhdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..af945f9
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/icon_font.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/icon_font_big.png b/Android/folioreader/res/drawable-xhdpi/icon_font_big.png
new file mode 100755 (executable)
index 0000000..d630d81
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/icon_font_big.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/icon_font_small.png b/Android/folioreader/res/drawable-xhdpi/icon_font_small.png
new file mode 100755 (executable)
index 0000000..7e54db6
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/icon_font_small.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/icon_moon_normal.png b/Android/folioreader/res/drawable-xhdpi/icon_moon_normal.png
new file mode 100755 (executable)
index 0000000..8951d25
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/icon_moon_normal.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/icon_moon_sel.png b/Android/folioreader/res/drawable-xhdpi/icon_moon_sel.png
new file mode 100755 (executable)
index 0000000..ecc7275
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/icon_moon_sel.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/icon_sun_normal.png b/Android/folioreader/res/drawable-xhdpi/icon_sun_normal.png
new file mode 100755 (executable)
index 0000000..16772b2
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/icon_sun_normal.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/icon_sun_sel.png b/Android/folioreader/res/drawable-xhdpi/icon_sun_sel.png
new file mode 100755 (executable)
index 0000000..2af0105
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/icon_sun_sel.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/inset_big.png b/Android/folioreader/res/drawable-xhdpi/inset_big.png
new file mode 100644 (file)
index 0000000..fd26141
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/inset_big.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/inset_small.png b/Android/folioreader/res/drawable-xhdpi/inset_small.png
new file mode 100644 (file)
index 0000000..d1dcf52
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/inset_small.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/man_speech_icon.png b/Android/folioreader/res/drawable-xhdpi/man_speech_icon.png
new file mode 100755 (executable)
index 0000000..c6f6d9a
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/man_speech_icon.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/margin_big.png b/Android/folioreader/res/drawable-xhdpi/margin_big.png
new file mode 100644 (file)
index 0000000..9f37a8c
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/margin_big.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/margin_small.png b/Android/folioreader/res/drawable-xhdpi/margin_small.png
new file mode 100644 (file)
index 0000000..dcb6843
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/margin_small.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/next_icon.png b/Android/folioreader/res/drawable-xhdpi/next_icon.png
new file mode 100755 (executable)
index 0000000..43c6985
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/next_icon.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/pause_btn.png b/Android/folioreader/res/drawable-xhdpi/pause_btn.png
new file mode 100755 (executable)
index 0000000..952043c
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/pause_btn.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/play_icon.png b/Android/folioreader/res/drawable-xhdpi/play_icon.png
new file mode 100755 (executable)
index 0000000..bc3409a
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/play_icon.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/prev_con.png b/Android/folioreader/res/drawable-xhdpi/prev_con.png
new file mode 100755 (executable)
index 0000000..97bcc99
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/prev_con.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/seekbar_thumb.png b/Android/folioreader/res/drawable-xhdpi/seekbar_thumb.png
new file mode 100755 (executable)
index 0000000..aee1e7e
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/seekbar_thumb.png differ
diff --git a/Android/folioreader/res/drawable-xhdpi/trash.png b/Android/folioreader/res/drawable-xhdpi/trash.png
new file mode 100755 (executable)
index 0000000..0f82b5a
Binary files /dev/null and b/Android/folioreader/res/drawable-xhdpi/trash.png differ
diff --git a/Android/folioreader/res/drawable-xxhdpi/font_big.png b/Android/folioreader/res/drawable-xxhdpi/font_big.png
new file mode 100644 (file)
index 0000000..e462e74
Binary files /dev/null and b/Android/folioreader/res/drawable-xxhdpi/font_big.png differ
diff --git a/Android/folioreader/res/drawable-xxhdpi/font_small.png b/Android/folioreader/res/drawable-xxhdpi/font_small.png
new file mode 100644 (file)
index 0000000..528c2cd
Binary files /dev/null and b/Android/folioreader/res/drawable-xxhdpi/font_small.png differ
diff --git a/Android/folioreader/res/drawable-xxhdpi/ic_comment.png b/Android/folioreader/res/drawable-xxhdpi/ic_comment.png
new file mode 100644 (file)
index 0000000..e34d5df
Binary files /dev/null and b/Android/folioreader/res/drawable-xxhdpi/ic_comment.png differ
diff --git a/Android/folioreader/res/drawable-xxhdpi/ic_menu_all.png b/Android/folioreader/res/drawable-xxhdpi/ic_menu_all.png
new file mode 100644 (file)
index 0000000..dbe65df
Binary files /dev/null and b/Android/folioreader/res/drawable-xxhdpi/ic_menu_all.png differ
diff --git a/Android/folioreader/res/drawable-xxhdpi/icon_font.png b/Android/folioreader/res/drawable-xxhdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..22a4106
Binary files /dev/null and b/Android/folioreader/res/drawable-xxhdpi/icon_font.png differ
diff --git a/Android/folioreader/res/drawable-xxhdpi/inset_big.png b/Android/folioreader/res/drawable-xxhdpi/inset_big.png
new file mode 100644 (file)
index 0000000..d373095
Binary files /dev/null and b/Android/folioreader/res/drawable-xxhdpi/inset_big.png differ
diff --git a/Android/folioreader/res/drawable-xxhdpi/inset_small.png b/Android/folioreader/res/drawable-xxhdpi/inset_small.png
new file mode 100644 (file)
index 0000000..8b965ab
Binary files /dev/null and b/Android/folioreader/res/drawable-xxhdpi/inset_small.png differ
diff --git a/Android/folioreader/res/drawable-xxhdpi/margin_big.png b/Android/folioreader/res/drawable-xxhdpi/margin_big.png
new file mode 100644 (file)
index 0000000..2ef9af7
Binary files /dev/null and b/Android/folioreader/res/drawable-xxhdpi/margin_big.png differ
diff --git a/Android/folioreader/res/drawable-xxhdpi/margin_small.png b/Android/folioreader/res/drawable-xxhdpi/margin_small.png
new file mode 100644 (file)
index 0000000..d82ea5d
Binary files /dev/null and b/Android/folioreader/res/drawable-xxhdpi/margin_small.png differ
diff --git a/Android/folioreader/res/drawable-xxxhdpi/font_big.png b/Android/folioreader/res/drawable-xxxhdpi/font_big.png
new file mode 100644 (file)
index 0000000..ffb2330
Binary files /dev/null and b/Android/folioreader/res/drawable-xxxhdpi/font_big.png differ
diff --git a/Android/folioreader/res/drawable-xxxhdpi/font_small.png b/Android/folioreader/res/drawable-xxxhdpi/font_small.png
new file mode 100644 (file)
index 0000000..7ee4980
Binary files /dev/null and b/Android/folioreader/res/drawable-xxxhdpi/font_small.png differ
diff --git a/Android/folioreader/res/drawable-xxxhdpi/ic_comment.png b/Android/folioreader/res/drawable-xxxhdpi/ic_comment.png
new file mode 100644 (file)
index 0000000..f7e3259
Binary files /dev/null and b/Android/folioreader/res/drawable-xxxhdpi/ic_comment.png differ
diff --git a/Android/folioreader/res/drawable-xxxhdpi/ic_menu_all.png b/Android/folioreader/res/drawable-xxxhdpi/ic_menu_all.png
new file mode 100644 (file)
index 0000000..116ef33
Binary files /dev/null and b/Android/folioreader/res/drawable-xxxhdpi/ic_menu_all.png differ
diff --git a/Android/folioreader/res/drawable-xxxhdpi/icon_font.png b/Android/folioreader/res/drawable-xxxhdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..d34fd9b
Binary files /dev/null and b/Android/folioreader/res/drawable-xxxhdpi/icon_font.png differ
diff --git a/Android/folioreader/res/drawable-xxxhdpi/inset_big.png b/Android/folioreader/res/drawable-xxxhdpi/inset_big.png
new file mode 100644 (file)
index 0000000..002f2a3
Binary files /dev/null and b/Android/folioreader/res/drawable-xxxhdpi/inset_big.png differ
diff --git a/Android/folioreader/res/drawable-xxxhdpi/inset_small.png b/Android/folioreader/res/drawable-xxxhdpi/inset_small.png
new file mode 100644 (file)
index 0000000..4fe05bf
Binary files /dev/null and b/Android/folioreader/res/drawable-xxxhdpi/inset_small.png differ
diff --git a/Android/folioreader/res/drawable-xxxhdpi/margin_big.png b/Android/folioreader/res/drawable-xxxhdpi/margin_big.png
new file mode 100644 (file)
index 0000000..99cc5ce
Binary files /dev/null and b/Android/folioreader/res/drawable-xxxhdpi/margin_big.png differ
diff --git a/Android/folioreader/res/drawable-xxxhdpi/margin_small.png b/Android/folioreader/res/drawable-xxxhdpi/margin_small.png
new file mode 100644 (file)
index 0000000..9b0d905
Binary files /dev/null and b/Android/folioreader/res/drawable-xxxhdpi/margin_small.png differ
diff --git a/Android/folioreader/res/drawable/arrow_down.png b/Android/folioreader/res/drawable/arrow_down.png
new file mode 100755 (executable)
index 0000000..05e6a59
Binary files /dev/null and b/Android/folioreader/res/drawable/arrow_down.png differ
diff --git a/Android/folioreader/res/drawable/arrow_up.png b/Android/folioreader/res/drawable/arrow_up.png
new file mode 100755 (executable)
index 0000000..4412938
Binary files /dev/null and b/Android/folioreader/res/drawable/arrow_up.png differ
diff --git a/Android/folioreader/res/drawable/btn_contents_highlights.xml b/Android/folioreader/res/drawable/btn_contents_highlights.xml
new file mode 100755 (executable)
index 0000000..0107251
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--  res/drawable/rounded_edittext.xml -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle" >
+    <corners android:radius="3dp"/>
+    <!--<solid android:color="@color/transparent_black"/>-->
+    <stroke android:color="@color/app_green" android:width="2dp"/>
+</shape>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/btn_moon_selector.xml b/Android/folioreader/res/drawable/btn_moon_selector.xml
new file mode 100755 (executable)
index 0000000..883d4e5
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true" android:drawable="@drawable/icon_moon_sel"/>
+    <item android:state_focused="true" android:drawable="@drawable/icon_moon_sel"/>
+    <item android:state_pressed="true" android:drawable="@drawable/icon_moon_sel"/>
+    <item android:drawable="@drawable/icon_moon_normal"/>
+</selector>
diff --git a/Android/folioreader/res/drawable/btn_sun_selector.xml b/Android/folioreader/res/drawable/btn_sun_selector.xml
new file mode 100755 (executable)
index 0000000..4e863bd
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true" android:drawable="@drawable/icon_sun_sel"/>
+    <item android:state_focused="true" android:drawable="@drawable/icon_sun_sel"/>
+    <item android:state_pressed="true" android:drawable="@drawable/icon_sun_sel"/>
+    <item android:drawable="@drawable/icon_sun_normal"/>
+</selector>
diff --git a/Android/folioreader/res/drawable/content_highlight_back_selector_night_mode.xml b/Android/folioreader/res/drawable/content_highlight_back_selector_night_mode.xml
new file mode 100755 (executable)
index 0000000..2b3fc94
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true" android:drawable="@color/app_green"/>
+    <item android:state_focused="true" android:drawable="@color/app_green"/>
+    <item android:state_pressed="true" android:drawable="@color/app_green"/>
+    <item android:drawable="@android:color/black"/>
+</selector>
diff --git a/Android/folioreader/res/drawable/content_highlight_text_selector.xml b/Android/folioreader/res/drawable/content_highlight_text_selector.xml
new file mode 100755 (executable)
index 0000000..c453f88
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true" android:color="@color/white"/>
+    <item android:state_focused="true" android:color="@color/white"/>
+    <item android:state_pressed="true" android:color="@color/white"/>
+    <item android:color="@color/app_green"/>
+</selector>
diff --git a/Android/folioreader/res/drawable/dottet_line.xml b/Android/folioreader/res/drawable/dottet_line.xml
new file mode 100755 (executable)
index 0000000..da836b1
--- /dev/null
@@ -0,0 +1,14 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item>
+        <shape android:shape="line">
+            <solid android:color="#ffffff" />
+            <stroke
+                android:dashGap="5dp"
+                android:dashWidth="5dp"
+                android:width="1dp"
+                android:color="@color/black" />
+            <!--<padding
+                android:top="35dp" />-->
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/font_text_selector.xml b/Android/folioreader/res/drawable/font_text_selector.xml
new file mode 100755 (executable)
index 0000000..0762eb2
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true" android:color="@color/app_green"/>
+    <item android:state_focused="true" android:color="@color/app_green"/>
+    <item android:state_pressed="true" android:color="@color/app_green"/>
+    <item android:color="@color/app_gray"/>
+</selector>
diff --git a/Android/folioreader/res/drawable/ic_close_green_24dp.xml b/Android/folioreader/res/drawable/ic_close_green_24dp.xml
new file mode 100755 (executable)
index 0000000..e6e81ab
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="#71C851"
+        android:pathData="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
+</vector>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/ic_drawer_green_24dp.xml b/Android/folioreader/res/drawable/ic_drawer_green_24dp.xml
new file mode 100755 (executable)
index 0000000..a0b9d36
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="#71C851"
+        android:pathData="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
+</vector>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/ic_minus_black_24dp.xml b/Android/folioreader/res/drawable/ic_minus_black_24dp.xml
new file mode 100755 (executable)
index 0000000..a9e9c97
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="#000"
+        android:pathData="M19,13H5V11H19V13Z" />
+</vector>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/ic_minus_white_24dp.xml b/Android/folioreader/res/drawable/ic_minus_white_24dp.xml
new file mode 100755 (executable)
index 0000000..8e770da
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="#fff"
+        android:pathData="M19,13H5V11H19V13Z" />
+</vector>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/ic_offline_gray_48dp.xml b/Android/folioreader/res/drawable/ic_offline_gray_48dp.xml
new file mode 100755 (executable)
index 0000000..7ccb7df
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="#808080"
+        android:pathData="M7.73,10L15.73,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10M3,5.27L5.75,8C2.56,8.15 0,10.77 0,14A6,6 0 0,0 6,20H17.73L19.73,22L21,20.73L4.27,4M19.35,10.03C18.67,6.59 15.64,4 12,4C10.5,4 9.15,4.43 8,5.17L9.45,6.63C10.21,6.23 11.08,6 12,6A5.5,5.5 0 0,1 17.5,11.5V12H19A3,3 0 0,1 22,15C22,16.13 21.36,17.11 20.44,17.62L21.89,19.07C23.16,18.16 24,16.68 24,15C24,12.36 21.95,10.22 19.35,10.03Z" />
+</vector>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/ic_plus_black_24dp.xml b/Android/folioreader/res/drawable/ic_plus_black_24dp.xml
new file mode 100755 (executable)
index 0000000..ce4e94c
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#000" android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
+</vector>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/ic_plus_white_24dp.xml b/Android/folioreader/res/drawable/ic_plus_white_24dp.xml
new file mode 100755 (executable)
index 0000000..d3e15af
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="#fff"
+        android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
+</vector>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/ic_volume_gray_24dp.xml b/Android/folioreader/res/drawable/ic_volume_gray_24dp.xml
new file mode 100755 (executable)
index 0000000..6896915
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#000" android:pathData="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z" />
+</vector>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/icons_sroll.png b/Android/folioreader/res/drawable/icons_sroll.png
new file mode 100755 (executable)
index 0000000..63352f3
Binary files /dev/null and b/Android/folioreader/res/drawable/icons_sroll.png differ
diff --git a/Android/folioreader/res/drawable/note_edittext_background.xml b/Android/folioreader/res/drawable/note_edittext_background.xml
new file mode 100755 (executable)
index 0000000..f344669
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--  res/drawable/rounded_edittext.xml -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle" >
+    <corners android:radius="3dp"/>
+    <solid android:color="#FFFFFF"/>
+    <stroke android:color="@color/blue" android:width="2dp"/>
+</shape>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/popup.9.png b/Android/folioreader/res/drawable/popup.9.png
new file mode 100755 (executable)
index 0000000..3342272
Binary files /dev/null and b/Android/folioreader/res/drawable/popup.9.png differ
diff --git a/Android/folioreader/res/drawable/round_button.xml b/Android/folioreader/res/drawable/round_button.xml
new file mode 100755 (executable)
index 0000000..5520446
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle" android:padding="10dp">
+    <!-- you can use any color you want I used here gray color-->
+    <solid android:color="@color/app_green"/>
+    <corners android:radius="10dp"/>
+</shape>
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/style_back_color_selector.xml b/Android/folioreader/res/drawable/style_back_color_selector.xml
new file mode 100755 (executable)
index 0000000..0a911ab
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true" android:drawable="@color/app_green"/>
+    <item android:state_focused="true" android:drawable="@color/app_green"/>
+    <item android:state_pressed="true" android:drawable="@color/app_green"/>
+    <item android:drawable="@android:color/transparent"/>
+</selector>
diff --git a/Android/folioreader/res/drawable/style_text_color_selector.xml b/Android/folioreader/res/drawable/style_text_color_selector.xml
new file mode 100755 (executable)
index 0000000..d043613
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true"  android:color="@color/white"/>
+    <item android:state_focused="true" android:color="@color/white" />
+    <item android:state_pressed="true"  android:color="@color/white"/>
+    <item android:color="@color/app_gray"/>
+</selector>
diff --git a/Android/folioreader/res/drawable/thumb.xml b/Android/folioreader/res/drawable/thumb.xml
new file mode 100755 (executable)
index 0000000..17f8bff
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="oval">
+
+    <size android:height="8dp"
+          android:width="8dp"/>
+
+    <corners android:radius="12dp"/>
+
+    <solid android:color="@color/app_green"/>
+
+</shape>  
\ No newline at end of file
diff --git a/Android/folioreader/res/drawable/transparent_selector.xml b/Android/folioreader/res/drawable/transparent_selector.xml
new file mode 100755 (executable)
index 0000000..8591e2b
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true" android:drawable="@color/transparent_black"/>
+    <item android:state_focused="true" android:drawable="@color/transparent_black"/>
+    <item android:state_pressed="true" android:drawable="@color/transparent_black"/>
+    <item android:drawable="@android:color/transparent"/>
+</selector>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/action_item_horizontal.xml b/Android/folioreader/res/layout/action_item_horizontal.xml
new file mode 100755 (executable)
index 0000000..c6e7211
--- /dev/null
@@ -0,0 +1,28 @@
+<RelativeLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="horizontal"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:clickable="true"
+        android:focusable="true"
+        android:background="@drawable/transparent_selector">
+
+    <ImageView
+            android:id="@+id/iv_icon"
+            android:layout_centerHorizontal="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+
+    <TextView
+            android:id="@+id/tv_title"
+            android:layout_below="@+id/iv_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:gravity="center_horizontal"
+            android:paddingLeft="5dip"
+            android:paddingRight="5dip"
+            android:text="Chart"
+            android:textColor="#fff"/>
+
+</RelativeLayout>         
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/action_item_vertical.xml b/Android/folioreader/res/layout/action_item_vertical.xml
new file mode 100755 (executable)
index 0000000..b16fa21
--- /dev/null
@@ -0,0 +1,25 @@
+<LinearLayout\r
+        xmlns:android="http://schemas.android.com/apk/res/android"\r
+        android:orientation="horizontal"\r
+        android:layout_width="fill_parent"\r
+        android:layout_height="wrap_content"\r
+        android:clickable="true"\r
+        android:focusable="true"\r
+        android:background="@drawable/transparent_selector">\r
+\r
+    <ImageView\r
+            android:id="@+id/iv_icon"\r
+            android:layout_width="wrap_content"\r
+            android:layout_height="wrap_content"/>\r
+\r
+    <TextView\r
+            android:id="@+id/tv_title"\r
+            android:layout_width="fill_parent"\r
+            android:layout_height="fill_parent"\r
+            android:gravity="center_vertical"\r
+            android:paddingLeft="5dip"\r
+            android:paddingRight="10dip"\r
+            android:text="Chart"\r
+            android:textColor="#fff"/>\r
+\r
+</LinearLayout>         
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/activity_content_highlight.xml b/Android/folioreader/res/layout/activity_content_highlight.xml
new file mode 100755 (executable)
index 0000000..2a48ce4
--- /dev/null
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"\r
+    xmlns:app="http://schemas.android.com/apk/res-auto"\r
+    xmlns:tools="http://schemas.android.com/tools"\r
+    android:layout_width="match_parent"\r
+    android:layout_height="match_parent"\r
+    tools:context="com.folioreader.ui.folio.activity.ContentHighlightActivity">\r
+\r
+    <android.support.v7.widget.Toolbar\r
+        android:id="@+id/toolbar"\r
+        android:layout_width="match_parent"\r
+        android:layout_height="?attr/actionBarSize"\r
+        android:layout_margin="0dp"\r
+        android:background="@color/white"\r
+        android:padding="0dp"\r
+        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"\r
+        app:contentInsetEnd="0dp"\r
+        app:contentInsetLeft="0dp"\r
+        app:contentInsetRight="0dp"\r
+        app:contentInsetStart="0dp">\r
+\r
+        <RelativeLayout\r
+            android:layout_width="match_parent"\r
+            android:layout_height="match_parent">\r
+\r
+            <ImageView\r
+                android:id="@+id/btn_close"\r
+                android:layout_width="wrap_content"\r
+                android:layout_height="wrap_content"\r
+                android:layout_alignParentLeft="true"\r
+                android:layout_centerInParent="true"\r
+                android:layout_margin="16dp"\r
+                android:scaleType="centerCrop"\r
+                android:src="@drawable/ic_close_green_24dp" />\r
+\r
+            <TextView\r
+                android:id="@+id/tvTitle"\r
+                android:layout_width="wrap_content"\r
+                android:layout_height="wrap_content"\r
+                android:layout_centerInParent="true"\r
+                android:ellipsize="end"\r
+                android:gravity="center"\r
+                android:layout_toRightOf="@id/btn_close"\r
+                android:layout_marginStart="20dp"\r
+                android:maxLines="1"\r
+                android:text="@string/contents"\r
+                android:textColor="@android:color/white"\r
+                android:textSize="19dp" />\r
+\r
+            <LinearLayout android:id="@+id/layout_content_highlights"\r
+                android:layout_width="170dp"\r
+                android:layout_height="wrap_content"\r
+                android:layout_centerHorizontal="true"\r
+                android:layout_centerInParent="true"\r
+                android:orientation="horizontal"\r
+                android:padding="1.8dp">\r
+\r
+                <TextView\r
+                    android:id="@+id/btn_contents"\r
+                    android:layout_width="0dp"\r
+                    android:layout_height="match_parent"\r
+                    android:layout_weight="1"\r
+                    android:gravity="center|end"\r
+                    android:padding="5dp"\r
+                    android:text="@string/contents"\r
+                    android:textSize="16sp" />\r
+\r
+                <TextView\r
+                    android:id="@+id/btn_highlights"\r
+                    android:layout_width="0dp"\r
+                    android:layout_height="match_parent"\r
+                    android:layout_weight="1"\r
+                    android:gravity="center|start"\r
+                    android:padding="5dp"\r
+                    android:text="@string/highlights"\r
+                    android:textSize="16sp" />\r
+            </LinearLayout>\r
+\r
+            <View\r
+                android:id="@+id/view"\r
+                android:layout_width="match_parent"\r
+                android:layout_height="0.5dp"\r
+                android:layout_alignParentBottom="true"\r
+                android:background="@android:color/black" />\r
+        </RelativeLayout>\r
+    </android.support.v7.widget.Toolbar>\r
+\r
+    <FrameLayout\r
+        android:id="@+id/parent"\r
+        android:layout_width="match_parent"\r
+        android:layout_height="match_parent"\r
+        android:layout_below="@id/toolbar"/>\r
+</RelativeLayout>\r
diff --git a/Android/folioreader/res/layout/dialog_edit_notes.xml b/Android/folioreader/res/layout/dialog_edit_notes.xml
new file mode 100755 (executable)
index 0000000..89ed004
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="290dp"
+    android:layout_height="250dp"
+    android:gravity="center"
+    android:background="@android:color/transparent"
+    android:orientation="vertical">
+    <RelativeLayout
+        android:layout_width="290dp"
+        android:layout_height="250dp"
+        android:padding="10dp"
+        android:background="@color/white">
+        <TextView
+            android:id="@+id/lbl_heading"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="@string/edit_notes"
+            android:textSize="23sp" />
+        <EditText
+            android:id="@+id/edit_note"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:lines="5"
+            android:gravity="start"
+            android:layout_below="@id/lbl_heading"
+            android:inputType="textMultiLine"
+            android:scrollbars="vertical"
+            android:layout_marginTop="16dp"
+            android:background="@drawable/note_edittext_background"
+            android:padding="10dp"
+            android:textColor="@android:color/black" />
+        <Button
+            android:id="@+id/btn_save_note"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/edit_note"
+            android:layout_marginTop="8dp"
+            android:text="@string/save_note"
+            android:textColor="@android:color/white"
+            android:background="@color/app_green"
+            android:textAllCaps="false"
+            android:textSize="16sp" />
+    </RelativeLayout>
+</RelativeLayout>
diff --git a/Android/folioreader/res/layout/folio_activity.xml b/Android/folioreader/res/layout/folio_activity.xml
new file mode 100755 (executable)
index 0000000..b1ff310
--- /dev/null
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <com.folioreader.view.DirectionalViewpager
+        android:id="@+id/folioPageViewPager"
+        android:layout_below="@id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:direction="vertical" />
+
+    <android.support.v7.widget.Toolbar
+        android:id="@+id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="?attr/actionBarSize"
+        android:layout_margin="0dp"
+        android:background="@color/white"
+        android:contentInsetEnd="0dp"
+        android:contentInsetLeft="0dp"
+        android:contentInsetRight="0dp"
+        android:contentInsetStart="0dp"
+        android:padding="0dp"
+        app:contentInsetEnd="0dp"
+        app:contentInsetLeft="0dp"
+        app:contentInsetRight="0dp"
+        app:contentInsetStart="0dp">
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical"
+                android:orientation="horizontal">
+
+                <ImageView
+                    android:id="@+id/btn_close"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:padding="16dp"
+                    android:src="@drawable/ic_close_green_24dp" />
+
+                <ImageView
+                    android:id="@+id/btn_drawer"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:paddingEnd="16dp"
+                    android:paddingTop="16dp"
+                    android:paddingBottom="16dp"
+                    android:src="@drawable/ic_menu_all" />
+            </LinearLayout>
+
+            <TextView
+                android:id="@+id/lbl_center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerInParent="true"
+                android:layout_marginEnd="96dp"
+                android:layout_marginStart="96dp"
+                android:ellipsize="end"
+                android:gravity="center"
+                android:maxLines="1"
+                android:textColor="@android:color/black"
+                android:textSize="19dp" />
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_alignParentEnd="true"
+                android:layout_alignParentRight="true"
+                android:orientation="horizontal">
+
+                <ImageView
+                    android:id="@+id/btn_config"
+                    android:layout_width="24dp"
+                    android:layout_height="24dp"
+                    android:layout_marginEnd="16dp"
+                    android:scaleType="fitCenter"
+                    android:layout_gravity="center_vertical"
+                    android:src="@drawable/font_big" />
+
+                <ImageView
+                    android:id="@+id/btn_speaker"
+                    android:layout_width="24dp"
+                    android:layout_height="24dp"
+                    android:layout_marginStart="16dp"
+                    android:visibility="gone"
+                    android:layout_marginEnd="16dp"
+                    android:scaleType="fitCenter"
+                    android:layout_gravity="center_vertical"
+                    android:src="@drawable/ic_comment" />
+            </LinearLayout>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="0.5dp"
+                android:layout_alignParentBottom="true"
+                android:background="@android:color/black" />
+        </RelativeLayout>
+    </android.support.v7.widget.Toolbar>
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:id="@+id/shade"
+        android:visibility="gone"
+        android:background="#99000000">
+
+        <include layout="@layout/view_audio_player" />
+    </RelativeLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/folio_page_fragment.xml b/Android/folioreader/res/layout/folio_page_fragment.xml
new file mode 100755 (executable)
index 0000000..7a5098d
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/rlContainer"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <com.folioreader.view.ObservableWebView
+        android:id="@+id/contentWebView"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_above="@+id/indicatorLayout"
+        android:paddingBottom="2dp" />
+    <com.folioreader.view.VerticalSeekbar
+        android:id="@+id/scrollSeekbar"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_above="@+id/indicatorLayout"
+        android:layout_alignParentRight="true"
+        android:layout_marginRight="2dp"
+        android:animateLayoutChanges="true"
+        android:thumb="@drawable/thumb"
+        android:visibility="invisible" />
+    <LinearLayout
+        android:id="@+id/indicatorLayout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:gravity="center"
+        android:orientation="horizontal"
+        android:paddingBottom="2dp">
+        <TextView
+            android:visibility="gone"
+            android:id="@+id/minutesLeft"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="#888888"
+            android:textSize="7sp" />
+        <TextView
+            android:id="@+id/pagesLeft"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="#bbbbbb"
+            android:textSize="8sp" />
+    </LinearLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/fragment_contents.xml b/Android/folioreader/res/layout/fragment_contents.xml
new file mode 100755 (executable)
index 0000000..2760d16
--- /dev/null
@@ -0,0 +1,21 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.folioreader.ui.tableofcontents.view.TableOfContentFragment">
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/recycler_view_menu"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@android:color/white" />
+
+    <TextView
+        android:id="@+id/tv_error"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:textSize="23sp"
+        android:textStyle="bold"
+        android:visibility="gone" />
+</RelativeLayout>
diff --git a/Android/folioreader/res/layout/fragment_highlight_list.xml b/Android/folioreader/res/layout/fragment_highlight_list.xml
new file mode 100755 (executable)
index 0000000..985754e
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:id="@+id/rv_highlights"
+    android:layout_height="match_parent" />
+
diff --git a/Android/folioreader/res/layout/horiz_separator.xml b/Android/folioreader/res/layout/horiz_separator.xml
new file mode 100755 (executable)
index 0000000..59a85b1
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_width="wrap_content"
+        android:layout_height="fill_parent">
+    <TextView android:layout_height="fill_parent"
+              android:gravity="center"
+              android:layout_width="2px"
+              android:text=" "
+              android:background="#000000"/>
+</RelativeLayout>
diff --git a/Android/folioreader/res/layout/item_dictionary.xml b/Android/folioreader/res/layout/item_dictionary.xml
new file mode 100755 (executable)
index 0000000..59e5925
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/tv_word"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_margin="8dp"
+        android:textSize="17sp" />
+
+    <ImageButton
+        android:id="@+id/ib_speak"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_gravity="center_vertical"
+        android:background="@android:color/transparent"
+        android:src="@drawable/ic_volume_gray_24dp"
+        android:visibility="gone" />
+
+    <TextView
+        android:id="@+id/tv_definition"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/tv_word"
+        android:layout_margin="8dp" />
+
+    <TextView
+        android:id="@+id/tv_examples"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/tv_definition"
+        android:layout_marginLeft="8dp"
+        android:layout_marginRight="8dp" />
+</RelativeLayout>
diff --git a/Android/folioreader/res/layout/layout_dictionary.xml b/Android/folioreader/res/layout/layout_dictionary.xml
new file mode 100755 (executable)
index 0000000..9189a87
--- /dev/null
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <android.support.v7.widget.Toolbar
+        android:id="@+id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="?attr/actionBarSize"
+        android:layout_margin="0dp"
+        android:alpha="0.8"
+        android:background="@color/white"
+        android:padding="0dp"
+        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+        app:contentInsetEnd="0dp"
+        app:contentInsetLeft="0dp"
+        app:contentInsetRight="0dp"
+        app:contentInsetStart="0dp">
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="56dp">
+
+            <ImageView
+                android:id="@+id/btn_close"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentLeft="true"
+                android:layout_centerInParent="true"
+                android:layout_margin="16dp"
+                android:scaleType="centerCrop"
+                android:src="@drawable/ic_close_green_24dp" />
+
+            <LinearLayout
+                android:layout_width="170dp"
+                android:layout_height="wrap_content"
+                android:layout_centerHorizontal="true"
+                android:layout_centerInParent="true"
+                android:background="@drawable/btn_contents_highlights"
+                android:orientation="horizontal"
+                android:padding="1.8dp">
+
+                <TextView
+                    android:id="@+id/btn_dictionary"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/style_back_color_selector"
+                    android:gravity="center|end"
+                    android:padding="5dp"
+                    android:text="@string/dictionary"
+                    android:textColor="@drawable/content_highlight_text_selector"
+                    android:textSize="16sp" />
+
+                <TextView
+                    android:id="@+id/btn_wikipedia"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:background="@drawable/style_back_color_selector"
+                    android:gravity="center|start"
+                    android:padding="5dp"
+                    android:text="@string/wikipedia"
+                    android:textColor="@drawable/content_highlight_text_selector"
+                    android:textSize="16sp" />
+            </LinearLayout>
+        </RelativeLayout>
+    </android.support.v7.widget.Toolbar>
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_below="@+id/toolbar">
+
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/rv_dict_results"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scrollbars="vertical" />
+
+        <include layout="@layout/layout_wikipedia" />
+
+        <ProgressBar
+            android:id="@+id/progress"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true" />
+
+        <TextView
+            android:id="@+id/no_network"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:drawablePadding="8dp"
+            android:drawableTop="@drawable/ic_offline_gray_48dp"
+            android:text="offline"
+            android:visibility="gone" />
+
+        <Button
+            android:id="@+id/btn_google_search"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@+id/no_network"
+            android:layout_centerHorizontal="true"
+            android:layout_marginTop="8dp"
+            android:background="@drawable/round_button"
+            android:padding="8dp"
+            android:text="Google search"
+            android:visibility="gone" />
+
+    </RelativeLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/layout_wikipedia.xml b/Android/folioreader/res/layout/layout_wikipedia.xml
new file mode 100755 (executable)
index 0000000..194f874
--- /dev/null
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/ll_wiki"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/tv_word"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="8dp"
+        android:textSize="22sp"
+        android:textStyle="bold" />
+
+    <TextView
+        android:id="@+id/tv_def"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="8dp"
+        android:layout_marginRight="8dp"
+        android:textSize="17sp"
+        android:textStyle="italic" />
+
+    <WebView
+        android:id="@+id/wv_wiki"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="8dp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/popup_horizontal.xml b/Android/folioreader/res/layout/popup_horizontal.xml
new file mode 100755 (executable)
index 0000000..c3d1095
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+>
+
+    <ScrollView
+            android:id="@+id/scroller"
+            android:layout_marginTop="16dip"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/popup"
+            android:fadingEdgeLength="5dip"
+            android:scrollbars="none">
+
+        <LinearLayout
+                android:id="@+id/tracks"
+                android:orientation="horizontal"
+                android:layout_width="wrap_content"
+                android:layout_height="fill_parent"
+                android:layout_weight="1"
+                android:padding="10dip"/>
+
+    </ScrollView>
+
+    <ImageView
+            android:id="@+id/arrow_up"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/arrow_up"/>
+
+    <ImageView
+            android:id="@+id/arrow_down"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/scroller"
+            android:layout_marginTop="-4dip"
+            android:src="@drawable/arrow_down"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/popup_vertical.xml b/Android/folioreader/res/layout/popup_vertical.xml
new file mode 100755 (executable)
index 0000000..2a97b50
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<RelativeLayout\r
+        xmlns:android="http://schemas.android.com/apk/res/android"\r
+        android:layout_width="wrap_content"\r
+        android:layout_height="wrap_content"\r
+>\r
+\r
+    <ScrollView\r
+            android:id="@+id/scroller"\r
+            android:layout_marginTop="16dip"\r
+            android:layout_width="wrap_content"\r
+            android:layout_height="wrap_content"\r
+            android:background="@drawable/popup"\r
+            android:fadingEdgeLength="5dip"\r
+            android:scrollbars="none">\r
+\r
+        <LinearLayout\r
+                android:id="@+id/tracks"\r
+                android:orientation="vertical"\r
+                android:layout_width="wrap_content"\r
+                android:layout_height="wrap_content"\r
+                android:layout_weight="1"\r
+                android:padding="10dip"/>\r
+\r
+    </ScrollView>\r
+\r
+    <ImageView\r
+            android:id="@+id/arrow_up"\r
+            android:layout_width="wrap_content"\r
+            android:layout_height="wrap_content"\r
+            android:src="@drawable/arrow_up"/>\r
+\r
+    <ImageView\r
+            android:id="@+id/arrow_down"\r
+            android:layout_width="wrap_content"\r
+            android:layout_height="wrap_content"\r
+            android:layout_below="@id/scroller"\r
+            android:layout_marginTop="-4dip"\r
+            android:src="@drawable/arrow_down"/>\r
+\r
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/progress_dialog.xml b/Android/folioreader/res/layout/progress_dialog.xml
new file mode 100755 (executable)
index 0000000..5623598
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent">
+       <LinearLayout android:id="@+id/layout_loading"
+           android:layout_width="fill_parent"
+           android:layout_height="fill_parent"
+           android:orientation="vertical"
+           android:gravity="center">
+               <ProgressBar android:id="@+id/loading"
+               android:layout_width="wrap_content"
+               android:layout_height="wrap_content"
+               style="?android:attr/android:progressBarStyle"/>
+               <TextView android:id="@+id/label_loading"
+                   android:layout_width="wrap_content"
+                   android:layout_height="wrap_content"
+                   android:layout_marginTop="4dp"
+                   android:textSize="18sp"
+                   android:textColor="@android:color/white"
+                   android:text="@string/loading"/>
+       </LinearLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/row_font.xml b/Android/folioreader/res/layout/row_font.xml
new file mode 100755 (executable)
index 0000000..8f7783c
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@+id/name"
+          android:layout_width="wrap_content"
+          android:layout_height="match_parent"
+          android:textSize="24sp"
+          android:gravity="center_vertical"
+          android:padding="16dp"/>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/row_highlight.xml b/Android/folioreader/res/layout/row_highlight.xml
new file mode 100755 (executable)
index 0000000..8875584
--- /dev/null
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.daimajia.swipe.SwipeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <LinearLayout
+        android:id="@+id/swipe_linear_layout"
+        android:layout_width="160dp"
+        android:layout_height="0dp">
+
+        <ImageView
+            android:id="@+id/iv_edit_note"
+            android:layout_width="80dp"
+            android:layout_height="match_parent"
+            android:background="#8A2BE2"
+            android:paddingLeft="25dp"
+            android:paddingRight="25dp"
+            android:src="@drawable/edit_note" />
+
+        <ImageView
+            android:id="@+id/iv_delete"
+            android:layout_width="80dp"
+            android:layout_height="match_parent"
+            android:background="#FF3B30"
+            android:paddingLeft="25dp"
+            android:paddingRight="25dp"
+            android:src="@drawable/trash" />
+    </LinearLayout>
+
+    <RelativeLayout
+        android:id="@+id/container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:padding="8dp">
+
+        <TextView
+            android:id="@+id/tv_highlight_date"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="20 sep 2016"
+            android:textColor="@color/black"
+            android:textSize="14sp"
+            android:textStyle="bold" />
+
+        <com.folioreader.view.UnderlinedTextView
+            android:id="@+id/utv_highlight_content"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@+id/tv_highlight_date"
+            android:layout_marginBottom="8dp"
+            android:layout_marginTop="8dp"
+            android:ellipsize="end"
+            android:maxLines="3"
+            android:minLines="1"
+            android:textColor="@color/black"
+            android:textSize="17sp" />
+
+        <TextView
+            android:id="@+id/tv_note"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@+id/utv_highlight_content"
+            android:ellipsize="end"
+            android:text=""
+            android:textColor="@color/black"
+            android:textSize="14sp" />
+    </RelativeLayout>
+</com.daimajia.swipe.SwipeLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/row_table_of_contents.xml b/Android/folioreader/res/layout/row_table_of_contents.xml
new file mode 100755 (executable)
index 0000000..435b53c
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout android:id="@+id/container"
+              xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:app="http://schemas.android.com/apk/res-auto"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:gravity="center_vertical"
+              android:orientation="horizontal"
+              android:padding="5dp">
+
+    <ImageView
+        android:id="@+id/children"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingStart="5dp"
+        android:paddingEnd="10dp"/>
+
+    <com.folioreader.util.StyleableTextView
+        android:id="@+id/section_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:folio_font="SanFranciscoText-Regular.otf"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/view_audio_player.xml b/Android/folioreader/res/layout/view_audio_player.xml
new file mode 100755 (executable)
index 0000000..b529680
--- /dev/null
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/container"
+    android:visibility="invisible"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_alignParentBottom="true"
+    android:background="@color/white">
+
+    <LinearLayout
+        android:id="@+id/top_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:orientation="horizontal">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0.2"></LinearLayout>
+
+        <ImageButton
+            android:id="@+id/prev_button"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0.2"
+            android:background="@android:color/transparent"
+            android:src="@drawable/prev_con" />
+
+        <ImageButton
+            android:id="@+id/play_button"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0.2"
+            android:background="@android:color/transparent"
+            android:src="@drawable/play_icon" />
+
+        <ImageButton
+            android:id="@+id/next_button"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0.2"
+            android:background="@android:color/transparent"
+            android:src="@drawable/next_icon" />
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0.2"></LinearLayout>
+
+    </LinearLayout>
+
+    <View
+        android:id="@+id/first_separator"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_below="@+id/top_buttons"
+        android:background="@color/borders" />
+
+    <!--<android.support.v7.widget.RecyclerView
+        android:id="@+id/recycler_view_fonts"
+        android:scrollbars="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="80dp"
+        android:layout_below="@+id/first_separator"/>-->
+    <LinearLayout
+        android:id="@+id/playback_speed_Layout"
+        android:layout_width="match_parent"
+        android:layout_height="70dp"
+        android:layout_below="@+id/first_separator"
+        android:gravity="center"
+        android:orientation="horizontal"
+        android:weightSum="4.0">
+
+        <com.folioreader.view.StyleableTextView
+            android:id="@+id/btn_half_speed"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1.0"
+            android:gravity="center"
+            android:text="@string/half_speed"
+            android:textSize="14sp"
+            app:folio_font="@string/andada_font" />
+
+        <com.folioreader.view.StyleableTextView
+            android:id="@+id/btn_one_x_speed"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1.0"
+            android:gravity="center"
+            android:text="@string/onex"
+            android:textSize="17sp"
+            app:folio_font="@string/lato_font" />
+
+        <com.folioreader.view.StyleableTextView
+            android:id="@+id/btn_one_and_half_speed"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1.0"
+            android:gravity="center"
+            android:text="@string/one_and_half"
+            android:textSize="14sp"
+            app:folio_font="@string/lora_font" />
+
+        <com.folioreader.view.StyleableTextView
+            android:id="@+id/btn_twox_speed"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1.0"
+            android:gravity="center"
+            android:text="@string/two_x"
+            android:textSize="17sp"
+            app:folio_font="@string/raleway_font" />
+    </LinearLayout>
+
+    <View
+        android:id="@+id/second_separator"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_below="@+id/playback_speed_Layout"
+        android:background="@color/borders" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:layout_below="@+id/second_separator"
+        android:orientation="horizontal">
+
+        <com.folioreader.view.StyleableTextView
+            android:id="@+id/btn_backcolor_style"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1.0"
+            android:gravity="center"
+            android:text="@string/style"
+            android:textSize="17sp"
+            app:folio_font="@string/lato_font" />
+
+        <View
+            android:id="@+id/third_separator"
+            android:layout_width="1dp"
+            android:layout_height="match_parent"
+            android:background="@color/borders" />
+
+        <com.folioreader.view.StyleableTextView
+            android:id="@+id/btn_text_undeline_style"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1.0"
+            android:drawableBottom="@drawable/dottet_line"
+            android:gravity="center"
+            android:layerType="software"
+            android:text="@string/style_underline"
+            android:textSize="17sp"
+            app:folio_font="@string/lora_font" />
+
+        <View
+            android:id="@+id/fourth_separator"
+            android:layout_width="1dp"
+            android:layout_height="match_parent"
+            android:background="@color/borders" />
+
+        <com.folioreader.view.StyleableTextView
+            android:id="@+id/btn_text_color_style"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1.0"
+            android:gravity="center"
+            android:text="@string/style"
+            android:textSize="17sp"
+            app:folio_font="@string/raleway_font" />
+
+    </LinearLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/layout/view_config.xml b/Android/folioreader/res/layout/view_config.xml
new file mode 100755 (executable)
index 0000000..eaac533
--- /dev/null
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<RelativeLayout android:id="@+id/container"\r
+                xmlns:android="http://schemas.android.com/apk/res/android"\r
+                xmlns:app="http://schemas.android.com/apk/res-auto"\r
+                android:layout_width="match_parent"\r
+                android:layout_height="wrap_content"\r
+                android:background="@color/white">\r
+\r
+    <LinearLayout\r
+        android:id="@+id/top_buttons"\r
+        android:layout_width="match_parent"\r
+        android:layout_height="50dp"\r
+        android:orientation="horizontal">\r
+\r
+        <ImageButton\r
+            android:id="@+id/day_button"\r
+            android:layout_width="0dp"\r
+            android:layout_height="match_parent"\r
+            android:layout_weight="0.5"\r
+            android:background="@android:color/transparent"\r
+            android:src="@drawable/icon_sun_normal"/>\r
+\r
+        <ImageButton\r
+            android:id="@+id/night_button"\r
+            android:layout_width="0dp"\r
+            android:layout_height="match_parent"\r
+            android:layout_weight="0.5"\r
+            android:background="@android:color/transparent"\r
+            android:src="@drawable/icon_moon_normal"/>\r
+    </LinearLayout>\r
+\r
+    <View\r
+        android:id="@+id/first_separator"\r
+        android:layout_width="match_parent"\r
+        android:layout_height="1dp"\r
+        android:layout_below="@+id/top_buttons"\r
+        android:layout_marginEnd="20dp"\r
+        android:layout_marginStart="20dp"\r
+        android:background="@color/grey_color"/>\r
+\r
+    <LinearLayout\r
+        android:id="@+id/fontLayout"\r
+        android:layout_width="match_parent"\r
+        android:layout_height="50dp"\r
+        android:layout_below="@+id/first_separator"\r
+        android:gravity="center"\r
+        android:orientation="horizontal"\r
+        android:weightSum="4.0">\r
+\r
+        <com.folioreader.view.StyleableTextView\r
+            android:id="@+id/btn_font_ebgaramond"\r
+            android:layout_width="0dp"\r
+            android:layout_height="wrap_content"\r
+            android:layout_weight="1.0"\r
+            android:gravity="center"\r
+            android:text="@string/ebgaramond"\r
+            android:textSize="17sp"\r
+            app:folio_font="@string/ebgaramond_font"/>\r
+\r
+        <com.folioreader.view.StyleableTextView\r
+            android:id="@+id/btn_font_lato"\r
+            android:layout_width="0dp"\r
+            android:layout_height="wrap_content"\r
+            android:layout_weight="1.0"\r
+            android:gravity="center"\r
+            android:text="@string/lato"\r
+            android:textSize="17sp"\r
+            app:folio_font="@string/lato_font"/>\r
+\r
+        <com.folioreader.view.StyleableTextView\r
+            android:id="@+id/btn_font_lora"\r
+            android:layout_width="0dp"\r
+            android:layout_height="wrap_content"\r
+            android:layout_weight="1.0"\r
+            android:gravity="center"\r
+            android:text="@string/lora"\r
+            android:textSize="17sp"\r
+            app:folio_font="@string/lora_font"/>\r
+\r
+        <com.folioreader.view.StyleableTextView\r
+            android:id="@+id/btn_font_raleway"\r
+            android:layout_width="0dp"\r
+            android:layout_height="wrap_content"\r
+            android:layout_weight="1.0"\r
+            android:gravity="center"\r
+            android:text="@string/raleway"\r
+            android:textSize="17sp"\r
+            app:folio_font="@string/raleway_font"/>\r
+    </LinearLayout>\r
+\r
+    <View\r
+        android:id="@+id/second_separator"\r
+        android:layout_width="match_parent"\r
+        android:layout_height="1dp"\r
+        android:layout_below="@+id/fontLayout"\r
+        android:layout_marginEnd="20dp"\r
+        android:layout_marginStart="20dp"\r
+        android:background="@color/grey_color"/>\r
+\r
+    <RelativeLayout\r
+        android:id="@+id/font_size_layout"\r
+        android:layout_width="match_parent"\r
+        android:layout_height="wrap_content"\r
+        android:layout_below="@+id/second_separator"\r
+        android:orientation="horizontal">\r
+\r
+        <ImageView\r
+            android:id="@+id/small_font"\r
+            android:layout_width="@dimen/config_icon_width"\r
+            android:layout_height="wrap_content"\r
+            android:layout_centerVertical="true"\r
+            android:padding="8dp"\r
+            android:src="@drawable/font_small"\r
+            android:tint="@color/config_gray"/>\r
+\r
+        <SeekBar\r
+            android:id="@+id/seekbar_font_size"\r
+            android:layout_width="match_parent"\r
+            android:layout_height="wrap_content"\r
+            android:layout_toLeftOf="@+id/big_font"\r
+            android:layout_toRightOf="@+id/small_font"\r
+            android:max="4"\r
+            android:maxHeight="0.2dp"\r
+            android:minHeight="0.2dp"\r
+            android:padding="16dp"/>\r
+\r
+        <ImageView\r
+            android:id="@+id/big_font"\r
+            android:layout_width="@dimen/config_icon_width"\r
+            android:layout_height="wrap_content"\r
+            android:layout_alignParentRight="true"\r
+            android:layout_centerVertical="true"\r
+            android:padding="8dp"\r
+            android:src="@drawable/font_big"\r
+            android:tint="@color/config_gray"/>\r
+    </RelativeLayout>\r
+\r
+    <RelativeLayout\r
+        android:id="@+id/margin_size_layout"\r
+        android:layout_width="match_parent"\r
+        android:layout_height="wrap_content"\r
+        android:layout_below="@+id/font_size_layout"\r
+        android:orientation="horizontal">\r
+\r
+        <ImageView\r
+            android:id="@+id/small_margin"\r
+            android:layout_width="@dimen/config_icon_width"\r
+            android:layout_height="wrap_content"\r
+            android:layout_centerVertical="true"\r
+            android:padding="8dp"\r
+            android:src="@drawable/margin_small"\r
+            android:tint="@color/config_gray"/>\r
+\r
+        <SeekBar\r
+            android:id="@+id/seekbar_margin_size"\r
+            android:layout_width="match_parent"\r
+            android:layout_height="wrap_content"\r
+            android:layout_toLeftOf="@+id/big_margin"\r
+            android:layout_toRightOf="@+id/small_margin"\r
+            android:max="4"\r
+            android:maxHeight="0.2dp"\r
+            android:minHeight="0.2dp"\r
+            android:padding="16dp"/>\r
+\r
+        <ImageView\r
+            android:id="@+id/big_margin"\r
+            android:layout_width="@dimen/config_icon_width"\r
+            android:layout_height="wrap_content"\r
+            android:layout_alignParentRight="true"\r
+            android:layout_centerVertical="true"\r
+            android:padding="8dp"\r
+            android:src="@drawable/margin_big"\r
+            android:tint="@color/config_gray"/>\r
+    </RelativeLayout>\r
+\r
+    <RelativeLayout\r
+        android:id="@+id/interline_size_layout"\r
+        android:layout_width="match_parent"\r
+        android:layout_height="wrap_content"\r
+        android:layout_below="@+id/margin_size_layout"\r
+        android:orientation="horizontal">\r
+\r
+        <ImageView\r
+            android:id="@+id/small_interline"\r
+            android:layout_width="@dimen/config_icon_width"\r
+            android:layout_height="wrap_content"\r
+            android:layout_centerVertical="true"\r
+            android:padding="8dp"\r
+            android:src="@drawable/inset_small"\r
+            android:tint="@color/config_gray"/>\r
+\r
+        <SeekBar\r
+            android:id="@+id/seekbar_interline_size"\r
+            android:layout_width="match_parent"\r
+            android:layout_height="wrap_content"\r
+            android:layout_toLeftOf="@+id/big_interline"\r
+            android:layout_toRightOf="@+id/small_interline"\r
+            android:max="4"\r
+            android:maxHeight="0.2dp"\r
+            android:minHeight="0.2dp"\r
+            android:padding="16dp"/>\r
+\r
+        <ImageView\r
+            android:id="@+id/big_interline"\r
+            android:layout_width="@dimen/config_icon_width"\r
+            android:layout_height="wrap_content"\r
+            android:layout_alignParentRight="true"\r
+            android:layout_centerVertical="true"\r
+            android:padding="8dp"\r
+            android:src="@drawable/inset_big"\r
+            android:tint="@color/config_gray"/>\r
+    </RelativeLayout>\r
+</RelativeLayout>
\ No newline at end of file
diff --git a/Android/folioreader/res/menu/context_menu.xml b/Android/folioreader/res/menu/context_menu.xml
new file mode 100755 (executable)
index 0000000..67da498
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+            android:id="@+id/copy"
+            android:icon="@drawable/ic_drawer"
+            android:showAsAction="always"
+            android:title="copy"></item>
+    <item
+            android:id="@+id/button2"
+            android:icon="@drawable/icon_close"
+            android:showAsAction="ifRoom"
+            android:title="close">
+    </item>
+</menu>
\ No newline at end of file
diff --git a/Android/folioreader/res/menu/menu_on_highlight.xml b/Android/folioreader/res/menu/menu_on_highlight.xml
new file mode 100755 (executable)
index 0000000..3e8d364
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/menu_highlight_options" android:title=""
+          android:icon="@drawable/colors_marker" android:onClick="onContextualMenuItemClicked"/>
+    <item android:id="@+id/menu_delete" android:title="" android:icon="@drawable/ic_action_discard"
+          android:onClick="onContextualMenuItemClicked"/>
+    <item android:id="@+id/menu_share" android:title="" android:icon="@drawable/ic_action_share"
+          android:onClick="onContextualMenuItemClicked"/>
+</menu>
\ No newline at end of file
diff --git a/Android/folioreader/res/menu/menu_text_selection.xml b/Android/folioreader/res/menu/menu_text_selection.xml
new file mode 100755 (executable)
index 0000000..aff0628
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/menu_copy" android:title="Copy"
+          android:onClick="onContextualMenuItemClicked"/>
+    <item android:id="@+id/menu_highlight" android:title="Highlight"
+          android:onClick="onContextualMenuItemClicked"/>
+    <item android:id="@+id/menu_define" android:title="Define"
+          android:onClick="onContextualMenuItemClicked"/>
+    <item android:id="@+id/menu_share" android:title="" android:icon="@drawable/ic_action_share"
+          android:onClick="onContextualMenuItemClicked"/>
+</menu>
\ No newline at end of file
diff --git a/Android/folioreader/res/mipmap-hdpi/ic_launcher.png b/Android/folioreader/res/mipmap-hdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..cde69bc
Binary files /dev/null and b/Android/folioreader/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Android/folioreader/res/mipmap-mdpi/ic_launcher.png b/Android/folioreader/res/mipmap-mdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..c133a0c
Binary files /dev/null and b/Android/folioreader/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Android/folioreader/res/mipmap-xhdpi/ic_launcher.png b/Android/folioreader/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..bfa42f0
Binary files /dev/null and b/Android/folioreader/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Android/folioreader/res/mipmap-xxhdpi/ic_launcher.png b/Android/folioreader/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..324e72c
Binary files /dev/null and b/Android/folioreader/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Android/folioreader/res/res/drawable-hdpi/icon_font.png b/Android/folioreader/res/res/drawable-hdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..e2b5e1a
Binary files /dev/null and b/Android/folioreader/res/res/drawable-hdpi/icon_font.png differ
diff --git a/Android/folioreader/res/res/drawable-mdpi/icon_font.png b/Android/folioreader/res/res/drawable-mdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..850a7e8
Binary files /dev/null and b/Android/folioreader/res/res/drawable-mdpi/icon_font.png differ
diff --git a/Android/folioreader/res/res/drawable-xhdpi/icon_font.png b/Android/folioreader/res/res/drawable-xhdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..af280c5
Binary files /dev/null and b/Android/folioreader/res/res/drawable-xhdpi/icon_font.png differ
diff --git a/Android/folioreader/res/res/drawable-xxhdpi/icon_font.png b/Android/folioreader/res/res/drawable-xxhdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..ea73d5d
Binary files /dev/null and b/Android/folioreader/res/res/drawable-xxhdpi/icon_font.png differ
diff --git a/Android/folioreader/res/res/drawable-xxxhdpi/icon_font.png b/Android/folioreader/res/res/drawable-xxxhdpi/icon_font.png
new file mode 100755 (executable)
index 0000000..3463dad
Binary files /dev/null and b/Android/folioreader/res/res/drawable-xxxhdpi/icon_font.png differ
diff --git a/Android/folioreader/res/values-w820dp/dimens.xml b/Android/folioreader/res/values-w820dp/dimens.xml
new file mode 100755 (executable)
index 0000000..63fc816
--- /dev/null
@@ -0,0 +1,6 @@
+<resources>
+    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+         (such as screen margins) for screens with more than 820dp of available width. This
+         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+    <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/Android/folioreader/res/values/attrs.xml b/Android/folioreader/res/values/attrs.xml
new file mode 100755 (executable)
index 0000000..8d331c8
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="StyleableTextView">
+        <attr name="folio_font" format="string"/>
+    </declare-styleable>
+
+    <declare-styleable name="DirectionalViewpager">
+        <attr name="direction" format="string">
+           <!-- <enum name="vertical" value="vertical"></enum>
+            <enum name="horizontal" value="horizontal"></enum>-->
+        </attr>
+    </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/Android/folioreader/res/values/colors.xml b/Android/folioreader/res/values/colors.xml
new file mode 100755 (executable)
index 0000000..98197ef
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="transparent_black">#80000000</color>
+    <color name="borders">#F1EFF2</color>
+    <color name="black">#000000</color>
+    <color name="night">#322D32</color>
+    <color name="dark_night">#131313</color>
+    <color name="app_green">#71C951</color>
+    <color name="app_gray">#bcbcbc</color>
+    <color name="yellow">#FFEB6B</color>
+    <color name="orange">#F49509</color>
+    <color name="green">#C0ED72</color>
+    <color name="blue">#ADD8FF</color>
+    <color name="pink">#FFB0CA</color>
+    <color name="gray_text">#B6B6B6</color>
+    <color name="red">#ff0000</color>
+    <color name="text_color">#767676</color>
+    <color name="white">#FFFFFF</color>
+    <color name="underline">#F02814</color>
+    <color name="grey_color">#a8a8a8</color>
+
+    <color name="toolbar_background">#FF206F76</color>
+    <color name="toolbar_icons">#ffffff</color>
+    <color name="settings_icons">#FF206F76</color>
+    <color name="config_gray">#AEAEAE</color>
+</resources>
\ No newline at end of file
diff --git a/Android/folioreader/res/values/dimens.xml b/Android/folioreader/res/values/dimens.xml
new file mode 100755 (executable)
index 0000000..5be5127
--- /dev/null
@@ -0,0 +1,6 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+    <dimen name="config_icon_width">50dp</dimen>
+</resources>
diff --git a/Android/folioreader/res/values/strings.xml b/Android/folioreader/res/values/strings.xml
new file mode 100755 (executable)
index 0000000..1c827d2
--- /dev/null
@@ -0,0 +1,96 @@
+<resources>\r
+    <string name="app_name">folioreader</string>\r
+    <string name="andada">Andada</string>\r
+    <string name="ebgaramond">Garamond</string>\r
+    <string name="lato">Lato</string>\r
+    <string name="lora">Lora</string>\r
+    <string name="raleway">Raleway</string>\r
+\r
+    <string name="andada_font">fonts/andada/Andada-Regular.otf</string>\r
+    <string name="ebgaramond_font">fonts/ebgaramond/EBGaramond-Regular.ttf</string>\r
+    <string name="lato_font">fonts/lato/Lato-Regular.ttf</string>\r
+    <string name="lora_font">fonts/lora/Lora-Regular.ttf</string>\r
+    <string name="raleway_font">fonts/raleway/Raleway-Regular.ttf</string>\r
+\r
+    <string name="minutes_left">%1$d minut ·&#160;</string>\r
+    <string name="minute_left">%1$d minuta ·&#160;</string>\r
+    <string name="less_than_minute">Mniej niż minuta ·&#160;</string>\r
+\r
+    <string name="copied">Skopiowano</string>\r
+    <string name="send_to">Prześlij do</string>\r
+\r
+    <string name="copy">Kopiuj</string>\r
+    <string name="share">Udostępnij</string>\r
+    <string name="define">Zdefiniuj</string>\r
+    <string name="highlight">Highlight</string>\r
+\r
+    <string name="pages_left">Pozostało %1$d stron</string>\r
+    <string name="page_left">Pozostała %1$d strona</string>\r
+    <string name="highlights">Highlight</string>\r
+    <string name="half_speed">1/2x</string>\r
+    <string name="onex">1x</string>\r
+    <string name="one_and_half">1&#160;1/2x</string>\r
+    <string name="two_x">2x</string>\r
+    <string name="style">Styl</string>\r
+    <string name="style_underline"><![CDATA[<u style=\" border-bottom: 1px dashed #bcbcbc;   text-decoration: none;\">Styl</u>]]></string>\r
+    <string name="full_path">full-path</string>\r
+    <string name="error_goToPage">goToPage error:</string>\r
+    <string name="zip">.zip</string>\r
+    <string name="htmlBodyTableOpen">&lt;html&gt;&lt;body&gt;&lt;table&gt;</string>\r
+    <string name="titlesMeta">&lt;tr&gt;&lt;td&gt;Titles:&lt;/td&gt;</string>\r
+    <string name="authorsMeta">&lt;tr&gt;&lt;td&gt;Authors:&lt;/td&gt;</string>\r
+    <string name="contributorsMeta">&lt;tr&gt;&lt;td&gt;Contributors:&lt;/td&gt;</string>\r
+    <string name="languageMeta">&lt;tr&gt;&lt;td&gt;Language:&lt;/td&gt;&lt;td&gt;</string>\r
+    <string name="publishersMeta">&lt;tr&gt;&lt;td&gt;Publishers:&lt;/td&gt;</string>\r
+    <string name="typesMeta">&lt;tr&gt;&lt;td&gt;Types:&lt;/td&gt;</string>\r
+    <string name="descriptionsMeta">&lt;tr&gt;&lt;td&gt;Descriptions:&lt;/td&gt;</string>\r
+    <string name="rightsMeta">&lt;tr&gt;&lt;td&gt;Rights:&lt;/td&gt;</string>\r
+    <string name="tablebodyhtmlClose">&lt;/table&gt;&lt;/body&gt;&lt;/html&gt;</string>\r
+    <string name="tocReference">&lt;tr&gt;&lt;td&gt;Table of Contents:&lt;/td&gt;</string>\r
+    <string name="change_Font">Zmień czcionkę</string>\r
+    <string name="change_Font_Color">Kolor czcionki:</string>\r
+    <string name="changeStyle">Zmień styl</string>\r
+    <string name="blue_rgb">#0000ff</string>\r
+    <string name="red_rgb">#ff0000</string>\r
+    <string name="green_rgb">#00ff00</string>\r
+    <string name="black_rgb">#000000</string>\r
+    <string name="white_rgb">#ffffff</string>\r
+    <string name="error_CannotChangeStyle">Nie można zmienić stylu!</string>\r
+    <string name="OK">OK</string>\r
+    <string name="Cancel">Anuluj</string>\r
+    <string name="SetSizeTitle">Rozmiar panelu:</string>\r
+    <string name="LanguageChooserTitle">Wybierz dwa języki:</string>\r
+    <string name="parallelTextBool">parallelTextBool</string>\r
+    <string name="loading">Ładowanie</string>\r
+    <string name="please_wait">Proszę czekać…</string>\r
+    <string name="one_and_half_speed"><![CDATA[1<sup>1</sup>/<sub>2</sub>x]]></string>\r
+    <string name="half_speed_text"><![CDATA[<sup>1</sup>/<sub>2</sub>x]]></string>\r
+    <string name="please_wait_till_audio_is_parsed">Proszę czekać na załadowanie pliku audio</string>\r
+    <string name="audio_mark_id">javascript:alert(audioMarkID(\'epub-media-overlay-playing\',\'%s\'))</string>\r
+    <string name="setmediaoverlaystyle">javascript:alert(setMediaOverlayStyle(\'%s\'))</string>\r
+    <string name="goto_highlight">javascript:alert(gotoHighlight(\'%s\'))</string>\r
+    <string name="getHighlightString">javascript:alert(getHighlightString(\'%s\'))</string>\r
+    <string name="sethighlightstyle">javascript:alert(setHighlightStyle(\'%s\'));</string>\r
+    <string name="css_tag"><![CDATA[<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\">]]></string>\r
+    <string name="script_tag"><![CDATA[<script type=\"text/javascript\" src=\"%s\"></script>]]></string>\r
+    <string name="td_tag"><![CDATA[<td>%s</td></tr>]]></string>\r
+    <string name="tr_tag"><![CDATA[<tr><td></td><td>%s</td></tr>]]></string>\r
+    <string name="pattern">\\{\\{(-?\\d+\\.?\\d*)\\,(-?\\d+\\.?\\d*)\\}\\,\\s\\{(-?\\d+\\.?\\d*)\\,(-?\\d+\\.?\\d*)\\}\\}</string>\r
+    <!--<string name="pattern2">\\{\\{(-?\\d+\\.?\\d*)\\,(-?\\d+\\.?\\d*)\\}\\,\\s\\{(-?\\d+\\.?\\d*)\\,(-?\\d+\\.?\\d*)\\}\\}</string>-->\r
+    <string name="script_tag_method_call"><![CDATA[<script type=\"text/javascript\">%s</script>]]></string>\r
+  <string name="underline_highlights">\r
+      <span style="border-bottom: 1px solid #ff0000;">%s</span>\r
+  </string>\r
+    <string name="edit_notes">Zapisz notatki</string>\r
+    <string name="save_note">Zapisz notatkę</string>\r
+    <string name="please_enter_note">Proszę wprowadzić notatkę</string>\r
+    <string name="contents">Spis treści</string>\r
+\r
+    <!-- TODO: Remove or change this placeholder text -->\r
+    <string name="hello_blank_fragment">Hello blank fragment</string>\r
+    <string name="debug_start_drag">Starting drag!</string>\r
+    <string name="debug_start_unable_drag">Starting unable to drag!</string>\r
+    <string name="dictionary">Dictionary</string>\r
+    <string name="wikipedia">Wikipedia</string>\r
+    <string name="cannot_access_epub_message">Cannot open epub it needs storage access !</string>\r
+</resources>\r
diff --git a/Android/folioreader/res/values/styles.xml b/Android/folioreader/res/values/styles.xml
new file mode 100755 (executable)
index 0000000..f8920c3
--- /dev/null
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.Light.DarkActionBar">
+        <item name="windowActionBar">false</item>
+        <item name="windowNoTitle">true</item>
+    </style>
+
+    <style name="Animations" />
+
+    <!-- PopDownMenu -->
+    <style name="Animations.PopDownMenu" />
+
+    <style name="Animations.PopDownMenu.Center">
+        <item name="android:windowEnterAnimation">@anim/grow_from_top</item>
+        <item name="android:windowExitAnimation">@anim/shrink_from_bottom</item>
+    </style>
+
+    <style name="Animations.PopDownMenu.Left">
+        <item name="android:windowEnterAnimation">@anim/grow_from_topleft_to_bottomright</item>
+        <item name="android:windowExitAnimation">@anim/shrink_from_bottomright_to_topleft</item>
+    </style>
+
+    <style name="Animations.PopDownMenu.Right">
+        <item name="android:windowEnterAnimation">@anim/grow_from_topright_to_bottomleft</item>
+        <item name="android:windowExitAnimation">@anim/shrink_from_bottomleft_to_topright</item>
+    </style>
+
+    <style name="Animations.PopDownMenu.Reflect">
+        <item name="android:windowEnterAnimation">@anim/pump_top</item>
+        <item name="android:windowExitAnimation">@anim/disappear</item>
+    </style>
+
+    <!-- PopUpMenu -->
+    <style name="Animations.PopUpMenu" />
+
+    <style name="Animations.PopUpMenu.Center">
+        <item name="android:windowEnterAnimation">@anim/grow_from_bottom</item>
+        <item name="android:windowExitAnimation">@anim/shrink_from_top</item>
+    </style>
+
+    <style name="Animations.PopUpMenu.Left">
+        <item name="android:windowEnterAnimation">@anim/grow_from_bottomleft_to_topright</item>
+        <item name="android:windowExitAnimation">@anim/shrink_from_topright_to_bottomleft</item>
+    </style>
+
+    <style name="Animations.PopUpMenu.Right">
+        <item name="android:windowEnterAnimation">@anim/grow_from_bottomright_to_topleft</item>
+        <item name="android:windowExitAnimation">@anim/shrink_from_topleft_to_bottomright</item>
+    </style>
+
+    <style name="Animations.PopUpMenu.Reflect">
+        <item name="android:windowEnterAnimation">@anim/pump_bottom</item>
+        <item name="android:windowExitAnimation">@anim/disappear</item>
+    </style>
+
+    <declare-styleable name="UnderlinedTextView">
+        <attr name="underlineWidth" format="dimension" />
+        <attr name="underlineColor" format="color" />
+    </declare-styleable>
+
+    <style name="full_screen_dialog" parent="@style/Theme.AppCompat">
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowIsFloating">true</item>
+        <item name="android:backgroundDimEnabled">true</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+    </style>
+
+    <style name="DialogCustomTheme" parent="android:Theme.Holo.Dialog.NoActionBar">
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+    </style>
+
+    <style name="DialogAnimation">
+        <item name="android:windowEnterAnimation">@anim/slide_up</item>
+        <item name="android:windowExitAnimation">@anim/slide_down</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/Android/folioreader/src/main/assets/css/Style.css b/Android/folioreader/src/main/assets/css/Style.css
new file mode 100755 (executable)
index 0000000..7450772
--- /dev/null
@@ -0,0 +1,345 @@
+/**
+ *  Style.css
+ *  FolioReaderKit
+ *
+ *  Created by Heberti Almeida on 06/05/15.
+ *  Copyright (c) 2015 Folio Reader. All rights reserved.
+ */
+
+/* CSS Reset */
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+    margin: 0;
+    vertical-align: baseline;
+}
+
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+    display: block;
+}
+
+/* ePUB */
+html {
+    -webkit-text-size-adjust: none; /* Never autoresize text */
+    padding: 0 0 !important;
+}
+
+body {
+    padding: 40px 20px !important;
+    overflow: !important;
+}
+
+/* Custom padding for tablets */
+@media only screen and (min-device-width: 768px){
+    body {
+        padding: 60px 80px !important;
+    }
+}
+
+/* Table */
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+tbody, tfoot, thead {
+    vertical-align: middle !important;
+}
+td, th, tr {
+    vertical-align: inherit !important;
+}
+
+/* List */
+dd, dir, menu, ol, ul { margin-left: 30px !important; }
+ol { list-style-type: decimal !important; }
+li { display: list-item !important; }
+ol ol, ol ul, ul ol, ul ul {
+    margin-bottom: 0 !important;
+    margin-top: 0 !important;
+}
+
+/* Links */
+a { -webkit-touch-callout: none; } /* Disable link callback */
+* { -webkit-user-select: text; }
+img { -webkit-user-select: none; }
+p {
+    margin: 1.3em 0 1.5em 0;
+    line-height: 1.40em !important;
+    text-indent: 1.25em;
+}
+b, strong, th {font-weight: bolder !important;}
+
+/* Forced font overrides */
+code, kbd, pre, samp, tt {
+    font-family: monospace, monospace !important;
+    font-size: 1em;
+}
+button, input, select, textarea { display: inline-block !important; }
+/*h1, h2, h3, h4, h5, h6 { font-weight: 400!important; }*/
+del, s, strike { text-decoration: line-through!important; }
+hr {
+    background-color: rgba(0,0,0,.1) !important;
+    border: none !important;
+    height: 1px !important;
+}
+
+
+/* Sub and Super */
+big { font-size: 1.15em !important; }
+small, sub, sup { font-size: .65em !important; }
+sub { vertical-align: sub !important; }
+sup {
+    font-family: monospace !important;
+    vertical-align: super !important;
+}
+
+
+/* iBooks like */
+a { text-decoration: none; }
+pre { white-space: pre-wrap; }
+@page { margin: 0 0 !important; }
+table, ol, il { text-align: -webkit-auto; }
+h1 ,h2 ,h3 ,h4 ,h5 ,h6 {
+    text-align: -webkit-auto;
+    text-rendering: optimizelegibility;
+}
+
+/* allow breaking of words on headers and anchors as they tend to be larger font size or contain longer words */
+a, h1, h2, h3, h4, h5, h6 {
+    word-break: break-word !important;
+    -webkit-hyphens: none !important;
+    hyphens: none !important;
+}
+
+/* Begin Ted */
+img, svg, audio, video {
+    max-height: 95% !important;
+    max-width: 100% !important;
+    box-sizing: border-box;
+    object-fit: contain;
+    page-break-inside: avoid;
+}
+
+/* End Ted */
+
+/* Divs are also used to size images so make sure the authors get what they intended */
+/* which is for the images boxed in them to be completely visible on screen */
+div { max-width: 100%; }
+aside[epub|type~="footnote"] { display: none !important; }
+ruby > rt, ruby > rp { -webkit-user-select: none; }
+* { -webkit-font-smoothing: subpixel-antialiased }
+
+
+/*
+ *
+ * Highlight classes
+ *
+ */
+
+lk {
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+}
+
+/* Remove tap highlight */
+input, textarea, button, highlight, select, a {
+    -webkit-tap-highlight-color: rgba(0,0,0,0);
+}
+
+/* Highlight styles */
+html .highlight_yellow {background:rgb(255, 235, 107)}
+html .highlight_green {background:#C0ED72}
+html .highlight_blue {background:#ADD8FF}
+html .highlight_pink {background:#FFB0CA}
+html .highlight_underline {
+    text-decoration: none;
+    border-bottom: 2px solid #F02814;
+}
+
+html .highlight_yellow, html .highlight_green, html .highlight_blue, html .highlight_pink, span.epub-media-overlay-playing {
+    border-radius: 3px;
+    padding: 0 2px;
+    margin: 0 -2px;
+}
+
+/* default media overlay style */
+.mediaOverlayStyle0 span.epub-media-overlay-playing {
+    background: #ccc
+}
+
+.mediaOverlayStyle1 .epub-media-overlay-playing {
+    border-bottom: dotted 2px transparent;
+    border-radius: 0;
+}
+
+
+
+/*
+ *
+ * Night mode
+ *
+ */
+
+html {
+    -webkit-transition: all 0.6s ease;
+    background-color: #FFFFFF !important;
+}
+
+body {
+    background-color: transparent !important;
+}
+
+html.nightMode {
+    background-color: #131313 !important;
+}
+
+.nightMode p, .nightMode div {
+    color: #767676 !important;
+    background-color: transparent !important;
+}
+
+.nightMode h1, .nightMode h2, .nightMode h3, .nightMode h4, .nightMode h5, .nightMode h6 {
+    color: #848484 !important;
+}
+
+html.nightMode .highlight_yellow {background:rgba(255, 235, 107, 0.9)}
+html.nightMode .highlight_green {background:rgba(192, 237, 114, 0.9)}
+html.nightMode .highlight_blue {background:rgba(173, 216, 255, 0.9)}
+html.nightMode .highlight_pink {background:rgba(255, 176, 202, 0.9)}
+html.nightMode .highlight_underline {border-bottom: 2px solid rgba(240, 40, 20, 0.6)}
+
+
+/*
+ *
+ * Font classes
+ *
+ */
+
+@font-face {
+    font-family: 'andada';
+    src: url('file:///android_asset/fonts/andada/Andada-Regular.otf');
+}
+
+@font-face {
+    font-family: 'garamond';
+    src: url('file:///android_asset/fonts/ebgaramond/EBGaramond-Regular.ttf');
+}
+
+@font-face {
+    font-family: 'lato';
+    src: url('file:///android_asset/fonts/lato/Lato-Regular.ttf');
+}
+
+@font-face {
+    font-family: 'lora';
+    src: url('file:///android_asset/fonts/lora/Lora-Regular.ttf');
+}
+
+@font-face {
+    font-family: 'raleway';
+    src: url('file:///android_asset/fonts/raleway/Raleway-Regular.ttf');
+}
+
+.andada, .andada p, .andada span, .andada div {
+     font-family: "andada", sans-serif !important;
+ }
+.garamond, .garamond p, .garamond span, .garamond div {
+     font-family: "garamond", sans-serif !important;
+ }
+.lato, .lato p, .lato span, .lato div {
+     font-family: "lato", serif !important;
+ }
+.lora, .lora p, .lora span, .lora div {
+     font-family: "lora", serif !important;
+ }
+.raleway, .raleway p, .raleway span, .raleway div {
+     font-family: "raleway", sans-serif !important;
+ }
+
+html.textSizeOne { font-size: 13px !important; }
+html.textSizeTwo { font-size: 15px !important; }
+html.textSizeThree { font-size: 17px !important; }
+html.textSizeFour { font-size: 19px !important; }
+html.textSizeFive { font-size: 21px !important; }
+
+h1 {
+    font-size: 2em;
+    line-height: 1.2;
+}
+h2 {
+    font-size: 1.5em;
+    line-height: 1.2;
+}
+h3 {
+    font-size: 1.17em;
+    line-height: 1.2;
+}
+h4 {
+    font-size: 1em;
+    line-height: 1.2;
+}
+h5 {
+    font-size: 0.83em;
+    line-height: 1.2;
+}
+h6 {
+    font-size: 0.67em;
+    line-height: 1.2;
+}
+body {
+    word-break: break-word !important;
+    -webkit-hyphens: auto !important;
+    hyphens: auto !important;
+}
+p, span, div {
+    font-size: 1em;
+    line-height: 1.5 !important;
+}
+@media only screen and (min-device-width: 600px) {
+    div {
+        font-size: 1em;
+        line-height: 1.438em !important;
+    }
+    body {
+        -webkit-hyphens: none !important;
+        hyphens: none !important;
+    }
+}
+
+/*
+ *
+ * Margin sizes
+ *
+ */
+html.marginSizeOne body { padding: 20px 10px !important; }
+html.marginSizeTwo body { padding: 20px 20px !important; }
+html.marginSizeThree body { padding: 20px 30px !important; }
+html.marginSizeFour body { padding: 20px 40px !important; }
+html.marginSizeFive body { padding: 20px 50px !important; }
+
+/*
+ *
+ * Interline sizes
+ *
+ */
+
+html.interlineSizeOne p { line-height: 1.00em !important; }
+html.interlineSizeTwo p { line-height: 1.40em !important; }
+html.interlineSizeThree p { line-height: 1.60em !important; }
+html.interlineSizeFour p { line-height: 2.00em !important; }
+html.interlineSizeFive p { line-height: 2.40em !important; }
+
+p.paragraph {
+       text-align: left !important;
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/assets/fonts/andada/Andada-Bold.otf b/Android/folioreader/src/main/assets/fonts/andada/Andada-Bold.otf
new file mode 100755 (executable)
index 0000000..31c0c7f
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/andada/Andada-Bold.otf differ
diff --git a/Android/folioreader/src/main/assets/fonts/andada/Andada-BoldItalic.otf b/Android/folioreader/src/main/assets/fonts/andada/Andada-BoldItalic.otf
new file mode 100755 (executable)
index 0000000..e8bc479
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/andada/Andada-BoldItalic.otf differ
diff --git a/Android/folioreader/src/main/assets/fonts/andada/Andada-Italic.otf b/Android/folioreader/src/main/assets/fonts/andada/Andada-Italic.otf
new file mode 100755 (executable)
index 0000000..38a558d
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/andada/Andada-Italic.otf differ
diff --git a/Android/folioreader/src/main/assets/fonts/andada/Andada-Regular.otf b/Android/folioreader/src/main/assets/fonts/andada/Andada-Regular.otf
new file mode 100755 (executable)
index 0000000..0532524
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/andada/Andada-Regular.otf differ
diff --git a/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Bold.ttf b/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Bold.ttf
new file mode 100755 (executable)
index 0000000..2f62af2
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Bold.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-BoldItalic.ttf b/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-BoldItalic.ttf
new file mode 100755 (executable)
index 0000000..d45f36c
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-BoldItalic.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Italic.ttf b/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Italic.ttf
new file mode 100755 (executable)
index 0000000..3abd48f
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Italic.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Regular.ttf b/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Regular.ttf
new file mode 100755 (executable)
index 0000000..ce358c4
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/ebgaramond/EBGaramond-Regular.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/lato/Lato-Bold.ttf b/Android/folioreader/src/main/assets/fonts/lato/Lato-Bold.ttf
new file mode 100755 (executable)
index 0000000..7434369
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/lato/Lato-Bold.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/lato/Lato-BoldItalic.ttf b/Android/folioreader/src/main/assets/fonts/lato/Lato-BoldItalic.ttf
new file mode 100755 (executable)
index 0000000..684aacf
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/lato/Lato-BoldItalic.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/lato/Lato-Italic.ttf b/Android/folioreader/src/main/assets/fonts/lato/Lato-Italic.ttf
new file mode 100755 (executable)
index 0000000..3d3b7a2
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/lato/Lato-Italic.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/lato/Lato-Regular.ttf b/Android/folioreader/src/main/assets/fonts/lato/Lato-Regular.ttf
new file mode 100755 (executable)
index 0000000..04ea8ef
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/lato/Lato-Regular.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/lora/Lora-Bold.ttf b/Android/folioreader/src/main/assets/fonts/lora/Lora-Bold.ttf
new file mode 100755 (executable)
index 0000000..fffb5c5
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/lora/Lora-Bold.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/lora/Lora-BoldItalic.ttf b/Android/folioreader/src/main/assets/fonts/lora/Lora-BoldItalic.ttf
new file mode 100755 (executable)
index 0000000..881a5e5
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/lora/Lora-BoldItalic.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/lora/Lora-Italic.ttf b/Android/folioreader/src/main/assets/fonts/lora/Lora-Italic.ttf
new file mode 100755 (executable)
index 0000000..2c63550
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/lora/Lora-Italic.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/lora/Lora-Regular.ttf b/Android/folioreader/src/main/assets/fonts/lora/Lora-Regular.ttf
new file mode 100755 (executable)
index 0000000..760d1ca
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/lora/Lora-Regular.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/raleway/Raleway-Bold.ttf b/Android/folioreader/src/main/assets/fonts/raleway/Raleway-Bold.ttf
new file mode 100755 (executable)
index 0000000..7aa37f0
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/raleway/Raleway-Bold.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/raleway/Raleway-BoldItalic.ttf b/Android/folioreader/src/main/assets/fonts/raleway/Raleway-BoldItalic.ttf
new file mode 100755 (executable)
index 0000000..1d1c6dd
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/raleway/Raleway-BoldItalic.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/raleway/Raleway-Italic.ttf b/Android/folioreader/src/main/assets/fonts/raleway/Raleway-Italic.ttf
new file mode 100755 (executable)
index 0000000..e46ac30
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/raleway/Raleway-Italic.ttf differ
diff --git a/Android/folioreader/src/main/assets/fonts/raleway/Raleway-Regular.ttf b/Android/folioreader/src/main/assets/fonts/raleway/Raleway-Regular.ttf
new file mode 100755 (executable)
index 0000000..c6ec2f0
Binary files /dev/null and b/Android/folioreader/src/main/assets/fonts/raleway/Raleway-Regular.ttf differ
diff --git a/Android/folioreader/src/main/assets/js/Bridge.js b/Android/folioreader/src/main/assets/js/Bridge.js
new file mode 100755 (executable)
index 0000000..4643631
--- /dev/null
@@ -0,0 +1,813 @@
+//\r
+//  Bridge.js\r
+//  FolioReaderKit\r
+//\r
+//  Created by Heberti Almeida on 06/05/15.\r
+//  Copyright (c) 2015 Folio Reader. All rights reserved.\r
+//\r
+\r
+var thisHighlight;\r
+var audioMarkClass;\r
+var wordsPerMinute = 180;\r
+\r
+document.addEventListener("DOMContentLoaded", function(event) {\r
+//    var lnk = document.getElementsByClassName("lnk");\r
+//    for (var i=0; i<lnk.length; i++) {\r
+//        lnk[i].setAttribute("onclick","return callVerseURL(this);");\r
+//    }\r
+});\r
+\r
+// Generate a GUID\r
+function guid() {\r
+    function s4() {\r
+        return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);\r
+    }\r
+    var guid = s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();\r
+    return guid.toUpperCase();\r
+}\r
+\r
+// Get All HTML\r
+function getHTML() {\r
+    Highlight.getHtmlAndSaveHighlight(document.documentElement.outerHTML);\r
+    //return document.documentElement.outerHTML;\r
+}\r
+\r
+// Class manipulation\r
+function hasClass(ele,cls) {\r
+  return !!ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));\r
+}\r
+\r
+function addClass(ele,cls) {\r
+  if (!hasClass(ele,cls)) ele.className += " "+cls;\r
+}\r
+\r
+function removeClass(ele,cls) {\r
+  if (hasClass(ele,cls)) {\r
+    var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');\r
+    ele.className=ele.className.replace(reg,' ');\r
+  }\r
+}\r
+\r
+// Font name class\r
+function setFontName(cls) {\r
+    var elm = document.documentElement;\r
+    removeClass(elm, "andada");\r
+    removeClass(elm, "lato");\r
+    removeClass(elm, "lora");\r
+    removeClass(elm, "raleway");\r
+    addClass(elm, cls);\r
+}\r
+\r
+// Toggle night mode\r
+function nightMode(enable) {\r
+    var elm = document.documentElement;\r
+    if(enable) {\r
+        addClass(elm, "nightMode");\r
+    } else {\r
+        removeClass(elm, "nightMode");\r
+    }\r
+}\r
+\r
+// Set font size\r
+function setFontSize(cls) {\r
+    var elm = document.documentElement;\r
+    removeClass(elm, "textSizeOne");\r
+    removeClass(elm, "textSizeTwo");\r
+    removeClass(elm, "textSizeThree");\r
+    removeClass(elm, "textSizeFour");\r
+    removeClass(elm, "textSizeFive");\r
+    addClass(elm, cls);\r
+}\r
+\r
+\r
+// Menu colors\r
+function setHighlightStyle(style) {\r
+    Highlight.getUpdatedHighlightId(thisHighlight.id, style);\r
+}\r
+\r
+function removeThisHighlight() {\r
+    return thisHighlight.id;\r
+}\r
+\r
+function removeHighlightById(elmId) {\r
+    var elm = document.getElementById(elmId);\r
+    elm.outerHTML = elm.innerHTML;\r
+    return elm.id;\r
+}\r
+\r
+function getHighlightContent() {\r
+    return thisHighlight.textContent\r
+}\r
+\r
+function getBodyText() {\r
+    return document.body.innerText;\r
+}\r
+\r
+// Method that returns only selected text plain\r
+var getSelectedText = function() {\r
+    return window.getSelection().toString();\r
+}\r
+\r
+// Method that gets the Rect of current selected text\r
+// and returns in a JSON format\r
+var getRectForSelectedText = function(elm) {\r
+    if (typeof elm === "undefined") elm = window.getSelection().getRangeAt(0);\r
+\r
+    var rect = elm.getBoundingClientRect();\r
+    return "{{" + rect.left + "," + rect.top + "}, {" + rect.width + "," + rect.height + "}}";\r
+}\r
+\r
+// Method that call that a hightlight was clicked\r
+// with URL scheme and rect informations\r
+var callHighlightURL = function(elm) {\r
+       event.stopPropagation();\r
+       var URLBase = "highlight://";\r
+    var currentHighlightRect = getRectForSelectedText(elm);\r
+    thisHighlight = elm;\r
+\r
+    window.location = URLBase + encodeURIComponent(currentHighlightRect);\r
+}\r
+\r
+// Reading time\r
+function getReadingTime() {\r
+    var text = document.body.innerText;\r
+    var totalWords = text.trim().split(/\s+/g).length;\r
+    var wordsPerSecond = wordsPerMinute / 60; //define words per second based on words per minute\r
+    var totalReadingTimeSeconds = totalWords / wordsPerSecond; //define total reading time in seconds\r
+    var readingTimeMinutes = Math.round(totalReadingTimeSeconds / 60);\r
+\r
+    return readingTimeMinutes;\r
+}\r
+\r
+/**\r
+ Get Vertical or Horizontal paged #anchor offset\r
+ */\r
+var getAnchorOffset = function(target, horizontal) {\r
+    var elem = document.getElementById(target);\r
+\r
+    if (!elem) {\r
+        elem = document.getElementsByName(target)[0];\r
+    }\r
+\r
+    if (horizontal) {\r
+        return document.body.clientWidth * Math.floor(elem.offsetTop / window.innerHeight);\r
+    }\r
+\r
+    return elem.offsetTop;\r
+}\r
+\r
+function scrollAnchor(id) {\r
+    window.location.hash = id;\r
+}\r
+\r
+function findElementWithID(node) {\r
+    if( !node || node.tagName == "BODY")\r
+        return null\r
+    else if( node.id )\r
+        return node\r
+    else\r
+        return findElementWithID(node)\r
+}\r
+\r
+function findElementWithIDInView() {\r
+\r
+    if(audioMarkClass) {\r
+        // attempt to find an existing "audio mark"\r
+        var el = document.querySelector("."+audioMarkClass)\r
+\r
+        // if that existing audio mark exists and is in view, use it\r
+        if( el && el.offsetTop > document.body.scrollTop && el.offsetTop < (window.innerHeight + document.body.scrollTop))\r
+            return el\r
+    }\r
+\r
+    // @NOTE: is `span` too limiting?\r
+    var els = document.querySelectorAll("span[id]")\r
+\r
+    for(indx in els) {\r
+        var element = els[indx];\r
+\r
+        // Horizontal scroll\r
+        if (document.body.scrollTop == 0) {\r
+            var elLeft = document.body.clientWidth * Math.floor(element.offsetTop / window.innerHeight);\r
+            // document.body.scrollLeft = elLeft;\r
+\r
+            if (elLeft == document.body.scrollLeft) {\r
+                return element;\r
+            }\r
+\r
+        // Vertical\r
+        } else if(element.offsetTop > document.body.scrollTop) {\r
+            return element;\r
+        }\r
+    }\r
+\r
+    return null\r
+}\r
+\r
+\r
+/**\r
+ Play Audio - called by native UIMenuController when a user selects a bit of text and presses "play"\r
+ */\r
+function playAudio() {\r
+    var sel = getSelection();\r
+    var node = null;\r
+\r
+    // user selected text? start playing from the selected node\r
+    if (sel.toString() != "") {\r
+        node = sel.anchorNode ? findElementWithID(sel.anchorNode.parentNode) : null;\r
+\r
+    // find the first ID'd element that is within view (it will\r
+    } else {\r
+        node = findElementWithIDInView()\r
+    }\r
+\r
+    playAudioFragmentID(node ? node.id : null)\r
+}\r
+\r
+\r
+/**\r
+ Play Audio Fragment ID - tells page controller to begin playing audio from the following ID\r
+ */\r
+function playAudioFragmentID(fragmentID) {\r
+    var URLBase = "play-audio://";\r
+    window.location = URLBase + (fragmentID?encodeURIComponent(fragmentID):"")\r
+}\r
+\r
+/**\r
+ Go To Element - scrolls the webview to the requested element\r
+ */\r
+function goToEl(el) {\r
+    var top = document.body.scrollTop;\r
+    var elTop = el.offsetTop - 20;\r
+    var bottom = window.innerHeight + document.body.scrollTop;\r
+    var elBottom = el.offsetHeight + el.offsetTop + 60\r
+\r
+    if(elBottom > bottom || elTop < top) {\r
+        document.body.scrollTop = el.offsetTop - 20\r
+    }\r
+\r
+    /* Set scroll left in case horz scroll is activated.\r
+\r
+        The following works because el.offsetTop accounts for each page turned\r
+        as if the document was scrolling vertical. We then divide by the window\r
+        height to figure out what page the element should appear on and set scroll left\r
+        to scroll to that page.\r
+    */\r
+    if( document.body.scrollTop == 0 ){\r
+        var elLeft = document.body.clientWidth * Math.floor(el.offsetTop / window.innerHeight);\r
+        document.body.scrollLeft = elLeft;\r
+    }\r
+\r
+    return el;\r
+}\r
+\r
+/**\r
+ Remove All Classes - removes the given class from all elements in the DOM\r
+ */\r
+function removeAllClasses(className) {\r
+    var els = document.body.getElementsByClassName(className)\r
+    if( els.length > 0 )\r
+    for( i = 0; i <= els.length; i++) {\r
+        els[i].classList.remove(className);\r
+    }\r
+}\r
+\r
+/**\r
+ Audio Mark ID - marks an element with an ID with the given class and scrolls to it\r
+ */\r
+function audioMarkID(className, id) {\r
+    if (audioMarkClass)\r
+        removeAllClasses(audioMarkClass);\r
+\r
+    audioMarkClass = className\r
+    var el = document.getElementById(id);\r
+\r
+    goToEl(el);\r
+    el.classList.add(className)\r
+}\r
+\r
+function setMediaOverlayStyle(style){\r
+    document.documentElement.classList.remove("mediaOverlayStyle0", "mediaOverlayStyle1", "mediaOverlayStyle2")\r
+    document.documentElement.classList.add(style)\r
+}\r
+\r
+function setMediaOverlayStyleColors(color, colorHighlight) {\r
+    var stylesheet = document.styleSheets[document.styleSheets.length-1];\r
+    stylesheet.insertRule(".mediaOverlayStyle0 span.epub-media-overlay-playing { background: "+colorHighlight+" !important }")\r
+    stylesheet.insertRule(".mediaOverlayStyle1 span.epub-media-overlay-playing { border-color: "+color+" !important }")\r
+    stylesheet.insertRule(".mediaOverlayStyle2 span.epub-media-overlay-playing { color: "+color+" !important }")\r
+}\r
+\r
+var currentIndex = -1;\r
+\r
+\r
+function findSentenceWithIDInView(els) {\r
+    // @NOTE: is `span` too limiting?\r
+    for(indx in els) {\r
+        var element = els[indx];\r
+\r
+        // Horizontal scroll\r
+        if (document.body.scrollTop == 0) {\r
+            var elLeft = document.body.clientWidth * Math.floor(element.offsetTop / window.innerHeight);\r
+            // document.body.scrollLeft = elLeft;\r
+\r
+            if (elLeft == document.body.scrollLeft) {\r
+                currentIndex = indx;\r
+                return element;\r
+            }\r
+\r
+        // Vertical\r
+        } else if(element.offsetTop > document.body.scrollTop) {\r
+            currentIndex = indx;\r
+            return element;\r
+        }\r
+    }\r
+\r
+    return null\r
+}\r
+\r
+function findNextSentenceInArray(els) {\r
+    if(currentIndex >= 0) {\r
+        currentIndex ++;\r
+        return els[currentIndex];\r
+    }\r
+\r
+    return null\r
+}\r
+\r
+function resetCurrentSentenceIndex() {\r
+    currentIndex = -1;\r
+}\r
+\r
+function rewindCurrentIndex() {\r
+    currentIndex = currentIndex-1;\r
+}\r
+\r
+function getSentenceWithIndex(className) {\r
+    var sentence;\r
+    var sel = getSelection();\r
+    var node = null;\r
+    var elements = document.querySelectorAll("span.sentence");\r
+\r
+    // Check for a selected text, if found start reading from it\r
+    if (sel.toString() != "") {\r
+        console.log(sel.anchorNode.parentNode);\r
+        node = sel.anchorNode.parentNode;\r
+\r
+        if (node.className == "sentence") {\r
+            sentence = node\r
+\r
+            for(var i = 0, len = elements.length; i < len; i++) {\r
+                if (elements[i] === sentence) {\r
+                    currentIndex = i;\r
+                    break;\r
+                }\r
+            }\r
+        } else {\r
+            sentence = findSentenceWithIDInView(elements);\r
+        }\r
+    } else if (currentIndex < 0) {\r
+        sentence = findSentenceWithIDInView(elements);\r
+    } else {\r
+        sentence = findNextSentenceInArray(elements);\r
+    }\r
+\r
+    var text = sentence.innerText || sentence.textContent;\r
+\r
+    goToEl(sentence);\r
+\r
+    if (audioMarkClass){\r
+        removeAllClasses(audioMarkClass);\r
+    }\r
+\r
+    audioMarkClass = className;\r
+    sentence.classList.add(className)\r
+    return text;\r
+}\r
+\r
+function wrappingSentencesWithinPTags(){\r
+    currentIndex = -1;\r
+    "use strict";\r
+\r
+    var rxOpen = new RegExp("<[^\\/].+?>"),\r
+    rxClose = new RegExp("<\\/.+?>"),\r
+    rxSupStart = new RegExp("^<sup\\b[^>]*>"),\r
+    rxSupEnd = new RegExp("<\/sup>"),\r
+    sentenceEnd = [],\r
+    rxIndex;\r
+\r
+    sentenceEnd.push(new RegExp("[^\\d][\\.!\\?]+"));\r
+    sentenceEnd.push(new RegExp("(?=([^\\\"]*\\\"[^\\\"]*\\\")*[^\\\"]*?$)"));\r
+    sentenceEnd.push(new RegExp("(?![^\\(]*?\\))"));\r
+    sentenceEnd.push(new RegExp("(?![^\\[]*?\\])"));\r
+    sentenceEnd.push(new RegExp("(?![^\\{]*?\\})"));\r
+    sentenceEnd.push(new RegExp("(?![^\\|]*?\\|)"));\r
+    sentenceEnd.push(new RegExp("(?![^\\\\]*?\\\\)"));\r
+    //sentenceEnd.push(new RegExp("(?![^\\/.]*\\/)")); // all could be a problem, but this one is problematic\r
+\r
+    rxIndex = new RegExp(sentenceEnd.reduce(function (previousValue, currentValue) {\r
+                                            return previousValue + currentValue.source;\r
+                                            }, ""));\r
+\r
+    function indexSentenceEnd(html) {\r
+        var index = html.search(rxIndex);\r
+\r
+        if (index !== -1) {\r
+            index += html.match(rxIndex)[0].length - 1;\r
+        }\r
+\r
+        return index;\r
+    }\r
+\r
+    function pushSpan(array, className, string, classNameOpt) {\r
+        if (!string.match('[a-zA-Z0-9]+')) {\r
+            array.push(string);\r
+        } else {\r
+            array.push('<span class="' + className + '">' + string + '</span>');\r
+        }\r
+    }\r
+\r
+    function addSupToPrevious(html, array) {\r
+        var sup = html.search(rxSupStart),\r
+        end = 0,\r
+        last;\r
+\r
+        if (sup !== -1) {\r
+            end = html.search(rxSupEnd);\r
+            if (end !== -1) {\r
+                last = array.pop();\r
+                end = end + 6;\r
+                array.push(last.slice(0, -7) + html.slice(0, end) + last.slice(-7));\r
+            }\r
+        }\r
+\r
+        return html.slice(end);\r
+    }\r
+\r
+    function paragraphIsSentence(html, array) {\r
+        var index = indexSentenceEnd(html);\r
+\r
+        if (index === -1 || index === html.length) {\r
+            pushSpan(array, "sentence", html, "paragraphIsSentence");\r
+            html = "";\r
+        }\r
+\r
+        return html;\r
+    }\r
+\r
+    function paragraphNoMarkup(html, array) {\r
+        var open = html.search(rxOpen),\r
+        index = 0;\r
+\r
+        if (open === -1) {\r
+            index = indexSentenceEnd(html);\r
+            if (index === -1) {\r
+                index = html.length;\r
+            }\r
+\r
+            pushSpan(array, "sentence", html.slice(0, index += 1), "paragraphNoMarkup");\r
+        }\r
+\r
+        return html.slice(index);\r
+    }\r
+\r
+    function sentenceUncontained(html, array) {\r
+        var open = html.search(rxOpen),\r
+        index = 0,\r
+        close;\r
+\r
+        if (open !== -1) {\r
+            index = indexSentenceEnd(html);\r
+            if (index === -1) {\r
+                index = html.length;\r
+            }\r
+\r
+            close = html.search(rxClose);\r
+            if (index < open || index > close) {\r
+                pushSpan(array, "sentence", html.slice(0, index += 1), "sentenceUncontained");\r
+            } else {\r
+                index = 0;\r
+            }\r
+        }\r
+\r
+        return html.slice(index);\r
+    }\r
+\r
+    function sentenceContained(html, array) {\r
+        var open = html.search(rxOpen),\r
+        index = 0,\r
+        close,\r
+        count;\r
+\r
+        if (open !== -1) {\r
+            index = indexSentenceEnd(html);\r
+            if (index === -1) {\r
+                index = html.length;\r
+            }\r
+\r
+            close = html.search(rxClose);\r
+            if (index > open && index < close) {\r
+                count = html.match(rxClose)[0].length;\r
+                pushSpan(array, "sentence", html.slice(0, close + count), "sentenceContained");\r
+                index = close + count;\r
+            } else {\r
+                index = 0;\r
+            }\r
+        }\r
+\r
+        return html.slice(index);\r
+    }\r
+\r
+    function anythingElse(html, array) {\r
+        pushSpan(array, "sentence", html, "anythingElse");\r
+\r
+        return "";\r
+    }\r
+\r
+    function guessSenetences() {\r
+        var paragraphs = document.getElementsByTagName("p");\r
+\r
+        Array.prototype.forEach.call(paragraphs, function (paragraph) {\r
+            var html = paragraph.innerHTML,\r
+                length = html.length,\r
+                array = [],\r
+                safety = 100;\r
+\r
+            while (length && safety) {\r
+                html = addSupToPrevious(html, array);\r
+                if (html.length === length) {\r
+                    if (html.length === length) {\r
+                        html = paragraphIsSentence(html, array);\r
+                        if (html.length === length) {\r
+                            html = paragraphNoMarkup(html, array);\r
+                            if (html.length === length) {\r
+                                html = sentenceUncontained(html, array);\r
+                                if (html.length === length) {\r
+                                    html = sentenceContained(html, array);\r
+                                    if (html.length === length) {\r
+                                        html = anythingElse(html, array);\r
+                                    }\r
+                                }\r
+                            }\r
+                        }\r
+                    }\r
+                }\r
+\r
+                length = html.length;\r
+                safety -= 1;\r
+            }\r
+\r
+            paragraph.innerHTML = array.join("");\r
+        });\r
+    }\r
+\r
+    guessSenetences();\r
+}\r
+\r
+// Class based onClick listener\r
+\r
+function addClassBasedOnClickListener(schemeName, querySelector, attributeName, selectAll) {\r
+       if (selectAll) {\r
+               // Get all elements with the given query selector\r
+               var elements = document.querySelectorAll(querySelector);\r
+               for (elementIndex = 0; elementIndex < elements.length; elementIndex++) {\r
+                       var element = elements[elementIndex];\r
+                       addClassBasedOnClickListenerToElement(element, schemeName, attributeName);\r
+               }\r
+       } else {\r
+               // Get the first element with the given query selector\r
+               var element = document.querySelector(querySelector);\r
+               addClassBasedOnClickListenerToElement(element, schemeName, attributeName);\r
+       }\r
+}\r
+\r
+function addClassBasedOnClickListenerToElement(element, schemeName, attributeName) {\r
+       // Get the content from the given attribute name\r
+       var attributeContent = element.getAttribute(attributeName);\r
+       // Add the on click logic\r
+       element.setAttribute("onclick", "onClassBasedListenerClick(\"" + schemeName + "\", \"" + encodeURIComponent(attributeContent) + "\");");\r
+}\r
+\r
+var onClassBasedListenerClick = function(schemeName, attributeContent) {\r
+       // Prevent the browser from performing the default on click behavior\r
+       event.preventDefault();\r
+       // Don't pass the click event to other elemtents\r
+       event.stopPropagation();\r
+       // Create parameters containing the click position inside the web view.\r
+       var positionParameterString = "/clientX=" + event.clientX + "&clientY=" + event.clientY;\r
+       // Set the custom link URL to the event\r
+       window.location = schemeName + "://" + attributeContent + positionParameterString;\r
+}\r
+\r
+function getHighlightString(style) {\r
+    var range = window.getSelection().getRangeAt(0);\r
+    var selectionContents = range.extractContents();\r
+    var elm = document.createElement("highlight");\r
+    var id = guid();\r
+\r
+    elm.appendChild(selectionContents);\r
+    elm.setAttribute("id", id);\r
+    elm.setAttribute("onclick","callHighlightURL(this);");\r
+    elm.setAttribute("class", style);\r
+\r
+    range.insertNode(elm);\r
+    thisHighlight = elm;\r
+\r
+    var params = [];\r
+    params.push({id: id, rect: getRectForSelectedText(elm)});\r
+    Highlight.getHighlightJson(JSON.stringify(params));\r
+}\r
+\r
+function gotoHighlight(highlightId){\r
+  var element = document.getElementById(highlightId.toString());\r
+  if(element != null) {\r
+    goToEl(element);\r
+  }\r
+}\r
+\r
+$(function(){\r
+  window.ssReader = Class({\r
+    $singleton: true,\r
+\r
+    init: function() {\r
+      rangy.init();\r
+\r
+      this.highlighter = rangy.createHighlighter();\r
+\r
+      this.highlighter.addClassApplier(rangy.createClassApplier("highlight_yellow", {\r
+        ignoreWhiteSpace: true,\r
+        tagNames: ["span", "a"]\r
+      }));\r
+\r
+      this.highlighter.addClassApplier(rangy.createClassApplier("highlight_green", {\r
+        ignoreWhiteSpace: true,\r
+        tagNames: ["span", "a"]\r
+      }));\r
+\r
+      this.highlighter.addClassApplier(rangy.createClassApplier("highlight_blue", {\r
+        ignoreWhiteSpace: true,\r
+        tagNames: ["span", "a"]\r
+      }));\r
+\r
+      this.highlighter.addClassApplier(rangy.createClassApplier("highlight_pink", {\r
+        ignoreWhiteSpace: true,\r
+        tagNames: ["span", "a"]\r
+      }));\r
+\r
+      this.highlighter.addClassApplier(rangy.createClassApplier("highlight_underline", {\r
+        ignoreWhiteSpace: true,\r
+        tagNames: ["span", "a"]\r
+      }));\r
+\r
+    },\r
+\r
+    setFontAndada: function(){\r
+      this.setFont("andada");\r
+    },\r
+\r
+    setFontLato: function(){\r
+      this.setFont("lato");\r
+    },\r
+\r
+    setFontPtSerif: function(){\r
+      this.setFont("pt-serif");\r
+    },\r
+\r
+    setFontPtSans: function(){\r
+      this.setFont("pt-sans");\r
+    },\r
+\r
+    base64encode: function(str){\r
+      return btoa(unescape(encodeURIComponent(str)));\r
+    },\r
+\r
+    base64decode: function(str){\r
+      return decodeURIComponent(escape(atob(str)));\r
+    },\r
+\r
+    clearSelection: function(){\r
+      if (window.getSelection) {\r
+        if (window.getSelection().empty) {  // Chrome\r
+          window.getSelection().empty();\r
+        } else if (window.getSelection().removeAllRanges) {  // Firefox\r
+          window.getSelection().removeAllRanges();\r
+        }\r
+      } else if (document.selection) {  // IE?\r
+        document.selection.empty();\r
+      }\r
+    },\r
+\r
+    // Public methods\r
+\r
+    setFont: function(fontName){\r
+      $("#ss-wrapper-font").removeClass().addClass("ss-wrapper-"+fontName);\r
+    },\r
+\r
+    setSize: function(size){\r
+      $("#ss-wrapper-size").removeClass().addClass("ss-wrapper-"+size);\r
+    },\r
+\r
+    setTheme: function(theme){\r
+      $("body, #ss-wrapper-theme").removeClass().addClass("ss-wrapper-"+theme);\r
+    },\r
+\r
+    setComment: function(comment, inputId){\r
+      $("#"+inputId).val(ssReader.base64decode(comment));\r
+      $("#"+inputId).trigger("input", ["true"]);\r
+    },\r
+\r
+    highlightSelection: function(color){\r
+      try {\r
+\r
+        this.highlighter.highlightSelection("highlight_" + color, null);\r
+        var range = window.getSelection().toString();\r
+        var params = {content: range,rangy: this.getHighlights(),color: color};\r
+        this.clearSelection();\r
+        Highlight.onReceiveHighlights(JSON.stringify(params));\r
+      } catch(err){\r
+        console.log("highlightSelection : " + err);\r
+      }\r
+    },\r
+\r
+    unHighlightSelection: function(){\r
+      try {\r
+        this.highlighter.unhighlightSelection();\r
+        Highlight.onReceiveHighlights(this.getHighlights());\r
+      } catch(err){}\r
+    },\r
+\r
+    getHighlights: function(){\r
+      try {\r
+        return this.highlighter.serialize();\r
+      } catch(err){}\r
+    },\r
+\r
+    setHighlights: function(serializedHighlight){\r
+      try {\r
+        this.highlighter.removeAllHighlights();\r
+        this.highlighter.deserialize(serializedHighlight);\r
+      } catch(err){}\r
+    },\r
+\r
+    removeAll: function(){\r
+      try {\r
+        this.highlighter.removeAllHighlights();\r
+      } catch(err){}\r
+    },\r
+\r
+    copy: function(){\r
+      SSBridge.onCopy(window.getSelection().toString());\r
+      this.clearSelection();\r
+    },\r
+\r
+    share: function(){\r
+      SSBridge.onShare(window.getSelection().toString());\r
+      this.clearSelection();\r
+    },\r
+\r
+    search: function(){\r
+      SSBridge.onSearch(window.getSelection().toString());\r
+      this.clearSelection();\r
+    }\r
+  });\r
+\r
+   if(typeof ssReader !== "undefined"){\r
+      ssReader.init();\r
+    }\r
+\r
+    $(".verse").click(function(){\r
+      SSBridge.onVerseClick(ssReader.base64encode($(this).attr("verse")));\r
+    });\r
+\r
+    $("code").each(function(i){\r
+      var textarea = $("<textarea class='textarea'/>").attr("id", "input-"+i).on("input propertychange", function(event, isInit) {\r
+        $(this).css({'height': 'auto', 'overflow-y': 'hidden'}).height(this.scrollHeight);\r
+        $(this).next().css({'height': 'auto', 'overflow-y': 'hidden'}).height(this.scrollHeight);\r
+\r
+        if (!isInit) {\r
+          var that = this;\r
+          if (timeout !== null) {\r
+            clearTimeout(timeout);\r
+          }\r
+          timeout = setTimeout(function () {\r
+            SSBridge.onCommentsClick(\r
+                ssReader.base64encode($(that).val()),\r
+                $(that).attr("id")\r
+            );\r
+          }, 1000);\r
+        }\r
+      });\r
+      var border = $("<div class='textarea-border' />");\r
+      var container = $("<div class='textarea-container' />");\r
+\r
+      $(textarea).appendTo(container);\r
+      $(border).appendTo(container);\r
+\r
+      $(this).after(container);\r
+    });\r
+  });\r
+\r
+function array_diff(array1, array2){\r
+    var difference = $.grep(array1, function(el) { return $.inArray(el,array2) < 0});\r
+    return difference.concat($.grep(array2, function(el) { return $.inArray(el,array1) < 0}));;\r
+}\r
diff --git a/Android/folioreader/src/main/assets/js/jquery-3.1.1.min.js b/Android/folioreader/src/main/assets/js/jquery-3.1.1.min.js
new file mode 100755 (executable)
index 0000000..4c5be4c
--- /dev/null
@@ -0,0 +1,4 @@
+/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */
+!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=r.isArray(d)))?(e?(e=!1,f=c&&r.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,M,e),g(f,c,N,e)):(f++,j.call(a,g(f,c,M,e),g(f,c,N,e),g(f,c,M,c.notifyWith))):(d!==M&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),
+a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},T=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function U(){this.expando=r.expando+U.uid++}U.uid=1,U.prototype={cache:function(a){var b=a[this.expando];return b||(b={},T(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){r.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(K)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var V=new U,W=new U,X=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Y=/[A-Z]/g;function Z(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:X.test(a)?JSON.parse(a):a)}function $(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Y,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=Z(c)}catch(e){}W.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return W.hasData(a)||V.hasData(a)},data:function(a,b,c){return W.access(a,b,c)},removeData:function(a,b){W.remove(a,b)},_data:function(a,b,c){return V.access(a,b,c)},_removeData:function(a,b){V.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=W.get(f),1===f.nodeType&&!V.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),$(f,d,e[d])));V.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){W.set(this,a)}):S(this,function(b){var c;if(f&&void 0===b){if(c=W.get(f,a),void 0!==c)return c;if(c=$(f,a),void 0!==c)return c}else this.each(function(){W.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=V.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var _=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,aa=new RegExp("^(?:([+-])=|)("+_+")([a-z%]*)$","i"),ba=["Top","Right","Bottom","Left"],ca=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},da=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function ea(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&aa.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var fa={};function ga(a){var b,c=a.ownerDocument,d=a.nodeName,e=fa[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),fa[d]=e,e)}function ha(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=V.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&ca(d)&&(e[f]=ga(d))):"none"!==c&&(e[f]="none",V.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ha(this,!0)},hide:function(){return ha(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){ca(this)?r(this).show():r(this).hide()})}});var ia=/^(?:checkbox|radio)$/i,ja=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c<d;c++)V.set(a[c],"globalEval",!b||V.get(b[c],"globalEval"))}var oa=/<|&#?\w+;/;function pa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(oa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ja.exec(f)||["",""])[1].toLowerCase(),i=la[h]||la._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==wa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===wa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&r.nodeName(this,"input"))return this.click(),!1},_default:function(a){return r.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ua:va,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:va,isPropagationStopped:va,isImmediatePropagationStopped:va,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ua,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ua,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ua,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&ra.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&sa.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return xa(this,a,b,c,d)},one:function(a,b,c,d){return xa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=va),this.each(function(){r.event.remove(this,a,c,b)})}});var ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/<script|<style|<link/i,Aa=/checked\s*(?:[^=]|=\s*.checked.)/i,Ba=/^true\/(.*)/,Ca=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}W.hasData(a)&&(h=W.access(a),i=r.extend({},h),W.set(b,i))}}function Ha(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ia.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ia(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,ma(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Fa),l=0;l<i;l++)j=h[l],ka.test(j.type||"")&&!V.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Ca,""),k))}return a}function Ja(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(ma(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&na(ma(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(ya,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);if(b)if(c)for(f=f||ma(a),g=g||ma(h),d=0,e=f.length;d<e;d++)Ga(f[d],g[d]);else Ga(a,h);return g=ma(h,"script"),g.length>0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(ma(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ia(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(ma(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var Ka=/^margin/,La=new RegExp("^("+_+")(?!px)[a-z%]+$","i"),Ma=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",qa.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,qa.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Na(a,b,c){var d,e,f,g,h=a.style;return c=c||Ma(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&La.test(g)&&Ka.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Oa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Pa=/^(none|table(?!-c[ea]).+)/,Qa={position:"absolute",visibility:"hidden",display:"block"},Ra={letterSpacing:"0",fontWeight:"400"},Sa=["Webkit","Moz","ms"],Ta=d.createElement("div").style;function Ua(a){if(a in Ta)return a;var b=a[0].toUpperCase()+a.slice(1),c=Sa.length;while(c--)if(a=Sa[c]+b,a in Ta)return a}function Va(a,b,c){var d=aa.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Wa(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ba[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ba[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ba[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ba[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ba[f]+"Width",!0,e)));return g}function Xa(a,b,c){var d,e=!0,f=Ma(a),g="border-box"===r.css(a,"boxSizing",!1,f);if(a.getClientRects().length&&(d=a.getBoundingClientRect()[b]),d<=0||null==d){if(d=Na(a,b,f),(d<0||null==d)&&(d=a.style[b]),La.test(d))return d;e=g&&(o.boxSizingReliable()||d===a.style[b]),d=parseFloat(d)||0}return d+Wa(a,b,c||(g?"border":"content"),e,f)+"px"}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Na(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=a.style;return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=aa.exec(c))&&e[1]&&(c=ea(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b);return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Na(a,b,d)),"normal"===e&&b in Ra&&(e=Ra[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Pa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?Xa(a,b,d):da(a,Qa,function(){return Xa(a,b,d)})},set:function(a,c,d){var e,f=d&&Ma(a),g=d&&Wa(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=aa.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Va(a,c,g)}}}),r.cssHooks.marginLeft=Oa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Na(a,"marginLeft"))||a.getBoundingClientRect().left-da(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ba[d]+b]=f[d]||f[d-2]||f[0];return e}},Ka.test(a)||(r.cssHooks[a+b].set=Va)}),r.fn.extend({css:function(a,b){return S(this,function(a,b,c){var d,e,f={},g=0;if(r.isArray(b)){for(d=Ma(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function fb(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&ca(a),q=V.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],_a.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=V.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ha([a],!0),j=a.style.display||j,k=r.css(a,"display"),ha([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=V.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ha([a],!0),m.done(function(){p||ha([a]),V.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=eb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function gb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],r.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function hb(a,b,c){var d,e,f=0,g=hb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Za||cb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:Za||cb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(gb(k,j.opts.specialEasing);f<g;f++)if(d=hb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,eb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}r.Animation=r.extend(hb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return ea(c.elem,a,aa.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(K);for(var c,d=0,e=a.length;d<e;d++)c=a[d],hb.tweeners[c]=hb.tweeners[c]||[],hb.tweeners[c].unshift(b)},prefilters:[fb],prefilter:function(a,b){b?hb.prefilters.unshift(a):hb.prefilters.push(a)}}),r.speed=function(a,b,c){var e=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off||d.hidden?e.duration=0:"number"!=typeof e.duration&&(e.duration in r.fx.speeds?e.duration=r.fx.speeds[e.duration]:e.duration=r.fx.speeds._default),null!=e.queue&&e.queue!==!0||(e.queue="fx"),e.old=e.complete,e.complete=function(){r.isFunction(e.old)&&e.old.call(this),e.queue&&r.dequeue(this,e.queue)},e},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(ca).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=hb(this,r.extend({},a),f);(e||V.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=V.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&ab.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=V.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(db(b,!0),a,d,e)}}),r.each({slideDown:db("show"),slideUp:db("hide"),slideToggle:db("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(Za=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),Za=void 0},r.fx.timer=function(a){r.timers.push(a),a()?r.fx.start():r.timers.pop()},r.fx.interval=13,r.fx.start=function(){$a||($a=a.requestAnimationFrame?a.requestAnimationFrame(bb):a.setInterval(r.fx.tick,r.fx.interval))},r.fx.stop=function(){a.cancelAnimationFrame?a.cancelAnimationFrame($a):a.clearInterval($a),$a=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var ib,jb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return S(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)),
+void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!r.nodeName(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Qb=[],Rb=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Qb.pop()||r.expando+"_"+rb++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Rb.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Rb.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Rb,"$1"+e):b.jsonp!==!1&&(b.url+=(sb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Qb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=B.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=pa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=mb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length};function Sb(a){return r.isWindow(a)?a:9===a.nodeType&&a.defaultView}r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),d.width||d.height?(e=f.ownerDocument,c=Sb(e),b=e.documentElement,{top:d.top+c.pageYOffset-b.clientTop,left:d.left+c.pageXOffset-b.clientLeft}):d):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),r.nodeName(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||qa})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return S(this,function(a,d,e){var f=Sb(a);return void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Oa(o.pixelPosition,function(a,c){if(c)return c=Na(a,b),La.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return S(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.parseJSON=JSON.parse,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Tb=a.jQuery,Ub=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Ub),b&&a.jQuery===r&&(a.jQuery=Tb),r},b||(a.jQuery=a.$=r),r});
diff --git a/Android/folioreader/src/main/assets/js/jsface.min.js b/Android/folioreader/src/main/assets/js/jsface.min.js
new file mode 100755 (executable)
index 0000000..5d677ef
--- /dev/null
@@ -0,0 +1 @@
+!function(t,o,r,e,n,p,i,c,u,f){function s(t){return t&&typeof t===o&&!(typeof t.length===r&&!t.propertyIsEnumerable(e))&&t||null}function l(t){return t&&typeof t===o&&typeof t.length===r&&!t.propertyIsEnumerable(e)&&t||null}function y(t){return t&&"function"==typeof t&&t||null}function a(t){return y(t)&&t.prototype&&t===t.prototype.constructor&&t||null}function $(t,o,r,e){r&&r.hasOwnProperty(t)||(e[t]=o)}function b(t,o,r){if(l(o))for(var e=o.length;--e>=0;)b(t,o[e],r);else{r=r||{constructor:1,$super:1,prototype:1,$superp:1};var n,p,i=a(t),c=a(o),u=t.prototype;if(s(o)||i)for(n in o)$(n,o[n],r,t,i,u);if(c){p=o.prototype;for(n in p)$(n,p[n],r,t,i,u)}i&&c&&b(u,o.prototype,r)}}function g(t){var o,r;Object.freeze(t);for(r in t)o=t[r],t.hasOwnProperty(r)&&"object"==typeof o&&!Object.isFrozen(o)&&g(o)}function O(t,o){o||(o=t,t=0);var r,e,n,p,i,c,u,f,s,l,y,a=0,$={constructor:1,$singleton:1,$static:1,$statics:1,prototype:1,$super:1,$superp:1,main:1,toString:0},b=O.plugins;o=("function"==typeof o?o():o)||{},e=o.hasOwnProperty("constructor")?o.constructor:null,n=o.$singleton,p=o.$statics||o.$static;for(i in b)$[i]=1;for(t=!t||t instanceof Array?t:[t],u=t&&t.length,s=t[0],r=n?function(){}:e?e:function(){s&&s.apply(this,arguments)},!n&&u&&(l=s.prototype&&s===s.prototype.constructor&&s,l?(y=function(){},y.prototype=l.prototype,y.prototype.constructor=y,r.prototype=new y,r.prototype.constructor=r,l.prototype.constructor=l):r.prototype=s),c=n?r:r.prototype;u>a;){f=t[a++];for(i in f)$[i]||(r[i]=f[i]);if(!n&&0!==a)for(i in f.prototype)$[i]||(c[i]=f.prototype[i])}for(i in o)if(!$[i]){var g=o[i];g&&(g.get||g.set)?(g.enumerable=!0,Object.defineProperty(c,i,g)):c[i]=g}for(i in p)r[i]=p[i];f=t&&s||t,r.$super=f,r.$superp=f&&f.prototype||f;for(i in b)b[i](r,t,o);return"function"==typeof o.main&&o.main.call(r,r),r}O.plugins={$ready:function h(t,o,r,e){for(var n,c,u,f=r.$ready,s=o?o.length:0,l=s,a=s&&o[0].$super;s--;)for(c=0;i>c&&(u=p[c],n=o[s],n===u[0]&&(u[1].call(n,t,o,r),l--),l);c++);a&&h(t,[a],r,!0),!e&&y(f)&&(f.call(t,t,o,r),p.push([t,f]),i++)},$const:function(t,o,r){var e,n=r.$const;for(e in n)Object.defineProperty(t,e,{enumerable:!0,value:n[e]}),"object"!=typeof t[e]||Object.isFrozen(t[e])||g(t[e])}},f={Class:O,extend:b,mapOrNil:s,arrayOrNil:l,functionOrNil:y,classOrNil:a},"undefined"!=typeof module&&module.exports?module.exports=f:(u=t.Class,t.Class=O,t.jsface=f,f.noConflict=function(){t.Class=u})}(this,"object","number","length",Object.prototype.toString,[],0);
\ No newline at end of file
diff --git a/Android/folioreader/src/main/assets/js/rangy-classapplier.js b/Android/folioreader/src/main/assets/js/rangy-classapplier.js
new file mode 100755 (executable)
index 0000000..41d9f8d
--- /dev/null
@@ -0,0 +1,1110 @@
+/**
+ * Class Applier module for Rangy.
+ * Adds, removes and toggles classes on Ranges and Selections
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+(function(factory, root) {
+    if (typeof define == "function" && define.amd) {
+        // AMD. Register as an anonymous module with a dependency on Rangy.
+        define(["./rangy-core"], factory);
+    } else if (typeof module != "undefined" && typeof exports == "object") {
+        // Node/CommonJS style
+        module.exports = factory( require("rangy") );
+    } else {
+        // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
+        factory(root.rangy);
+    }
+})(function(rangy) {
+    rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) {
+        var dom = api.dom;
+        var DomPosition = dom.DomPosition;
+        var contains = dom.arrayContains;
+        var util = api.util;
+        var forEach = util.forEach;
+
+
+        var defaultTagName = "span";
+        var createElementNSSupported = util.isHostMethod(document, "createElementNS");
+
+        function each(obj, func) {
+            for (var i in obj) {
+                if (obj.hasOwnProperty(i)) {
+                    if (func(i, obj[i]) === false) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+
+        function trim(str) {
+            return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
+        }
+
+        function classNameContainsClass(fullClassName, className) {
+            return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName);
+        }
+
+        // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation
+        function hasClass(el, className) {
+            if (typeof el.classList == "object") {
+                return el.classList.contains(className);
+            } else {
+                var classNameSupported = (typeof el.className == "string");
+                var elClass = classNameSupported ? el.className : el.getAttribute("class");
+                return classNameContainsClass(elClass, className);
+            }
+        }
+
+        function addClass(el, className,serializedHighlight) {
+            if (typeof el.classList == "object") {
+                el.classList.add(className);
+                el.setAttribute("id", serializedHighlight);
+                el.setAttribute("onclick","callHighlightURL(this)");
+            } else {
+                var classNameSupported = (typeof el.className == "string");
+                var elClass = classNameSupported ? el.className : el.getAttribute("class");
+                if (elClass) {
+                    if (!classNameContainsClass(elClass, className)) {
+                        elClass += " " + className;
+                    }
+                } else {
+                    elClass = className;
+                }
+                if (classNameSupported) {
+                    el.className = elClass;
+                } else {
+                    el.setAttribute("class", elClass);
+                }
+            }
+        }
+
+        var removeClass = (function() {
+            function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
+                return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
+            }
+
+            return function(el, className) {
+                if (typeof el.classList == "object") {
+                    el.classList.remove(className);
+                    el.removeAttribute('onclick');
+                    el.removeAttribute('id');
+                } else {
+                    var classNameSupported = (typeof el.className == "string");
+                    var elClass = classNameSupported ? el.className : el.getAttribute("class");
+                    elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer);
+                    if (classNameSupported) {
+                        el.className = elClass;
+                    } else {
+                        el.setAttribute("class", elClass);
+                    }
+                }
+            };
+        })();
+
+        function getClass(el) {
+            var classNameSupported = (typeof el.className == "string");
+            return classNameSupported ? el.className : el.getAttribute("class");
+        }
+
+        function sortClassName(className) {
+            return className && className.split(/\s+/).sort().join(" ");
+        }
+
+        function getSortedClassName(el) {
+            return sortClassName( getClass(el) );
+        }
+
+        function haveSameClasses(el1, el2) {
+            return getSortedClassName(el1) == getSortedClassName(el2);
+        }
+
+        function hasAllClasses(el, className) {
+            var classes = className.split(/\s+/);
+            for (var i = 0, len = classes.length; i < len; ++i) {
+                if (!hasClass(el, trim(classes[i]))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        function canTextBeStyled(textNode) {
+            var parent = textNode.parentNode;
+            return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName));
+        }
+
+        function movePosition(position, oldParent, oldIndex, newParent, newIndex) {
+            var posNode = position.node, posOffset = position.offset;
+            var newNode = posNode, newOffset = posOffset;
+
+            if (posNode == newParent && posOffset > newIndex) {
+                ++newOffset;
+            }
+
+            if (posNode == oldParent && (posOffset == oldIndex  || posOffset == oldIndex + 1)) {
+                newNode = newParent;
+                newOffset += newIndex - oldIndex;
+            }
+
+            if (posNode == oldParent && posOffset > oldIndex + 1) {
+                --newOffset;
+            }
+
+            position.node = newNode;
+            position.offset = newOffset;
+        }
+
+        function movePositionWhenRemovingNode(position, parentNode, index) {
+            if (position.node == parentNode && position.offset > index) {
+                --position.offset;
+            }
+        }
+
+        function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) {
+            // For convenience, allow newIndex to be -1 to mean "insert at the end".
+            if (newIndex == -1) {
+                newIndex = newParent.childNodes.length;
+            }
+
+            var oldParent = node.parentNode;
+            var oldIndex = dom.getNodeIndex(node);
+
+            forEach(positionsToPreserve, function(position) {
+                movePosition(position, oldParent, oldIndex, newParent, newIndex);
+            });
+
+            // Now actually move the node.
+            if (newParent.childNodes.length == newIndex) {
+                newParent.appendChild(node);
+            } else {
+                newParent.insertBefore(node, newParent.childNodes[newIndex]);
+            }
+        }
+
+        function removePreservingPositions(node, positionsToPreserve) {
+
+            var oldParent = node.parentNode;
+            var oldIndex = dom.getNodeIndex(node);
+
+            forEach(positionsToPreserve, function(position) {
+                movePositionWhenRemovingNode(position, oldParent, oldIndex);
+            });
+
+            dom.removeNode(node);
+        }
+
+        function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) {
+            var child, children = [];
+            while ( (child = node.firstChild) ) {
+                movePreservingPositions(child, newParent, newIndex++, positionsToPreserve);
+                children.push(child);
+            }
+            if (removeNode) {
+                removePreservingPositions(node, positionsToPreserve);
+            }
+            return children;
+        }
+
+        function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) {
+            return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve);
+        }
+
+        function rangeSelectsAnyText(range, textNode) {
+            var textNodeRange = range.cloneRange();
+            textNodeRange.selectNodeContents(textNode);
+
+            var intersectionRange = textNodeRange.intersection(range);
+            var text = intersectionRange ? intersectionRange.toString() : "";
+
+            return text != "";
+        }
+
+        function getEffectiveTextNodes(range) {
+            var nodes = range.getNodes([3]);
+
+            // Optimization as per issue 145
+
+            // Remove non-intersecting text nodes from the start of the range
+            var start = 0, node;
+            while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) {
+                ++start;
+            }
+
+            // Remove non-intersecting text nodes from the start of the range
+            var end = nodes.length - 1;
+            while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) {
+                --end;
+            }
+
+            return nodes.slice(start, end + 1);
+        }
+
+        function elementsHaveSameNonClassAttributes(el1, el2) {
+            if (el1.attributes.length != el2.attributes.length) return false;
+            for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
+                attr1 = el1.attributes[i];
+                name = attr1.name;
+                if (name != "class") {
+                    attr2 = el2.attributes.getNamedItem(name);
+                    if ( (attr1 === null) != (attr2 === null) ) return false;
+                    if (attr1.specified != attr2.specified) return false;
+                    if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
+                }
+            }
+            return true;
+        }
+
+        function elementHasNonClassAttributes(el, exceptions) {
+            for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
+                attrName = el.attributes[i].name;
+                if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        var getComputedStyleProperty = dom.getComputedStyleProperty;
+        var isEditableElement = (function() {
+            var testEl = document.createElement("div");
+            return typeof testEl.isContentEditable == "boolean" ?
+                function (node) {
+                    return node && node.nodeType == 1 && node.isContentEditable;
+                } :
+                function (node) {
+                    if (!node || node.nodeType != 1 || node.contentEditable == "false") {
+                        return false;
+                    }
+                    return node.contentEditable == "true" || isEditableElement(node.parentNode);
+                };
+        })();
+
+        function isEditingHost(node) {
+            var parent;
+            return node && node.nodeType == 1 &&
+                (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") ||
+                (isEditableElement(node) && !isEditableElement(node.parentNode)));
+        }
+
+        function isEditable(node) {
+            return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
+        }
+
+        var inlineDisplayRegex = /^inline(-block|-table)?$/i;
+
+        function isNonInlineElement(node) {
+            return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
+        }
+
+        // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
+        var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
+
+        function isUnrenderedWhiteSpaceNode(node) {
+            if (node.data.length == 0) {
+                return true;
+            }
+            if (htmlNonWhiteSpaceRegex.test(node.data)) {
+                return false;
+            }
+            var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
+            switch (cssWhiteSpace) {
+                case "pre":
+                case "pre-wrap":
+                case "-moz-pre-wrap":
+                    return false;
+                case "pre-line":
+                    if (/[\r\n]/.test(node.data)) {
+                        return false;
+                    }
+            }
+
+            // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
+            // non-inline element, it will not be rendered. This seems to be a good enough definition.
+            return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
+        }
+
+        function getRangeBoundaries(ranges) {
+            var positions = [], i, range;
+            for (i = 0; range = ranges[i++]; ) {
+                positions.push(
+                    new DomPosition(range.startContainer, range.startOffset),
+                    new DomPosition(range.endContainer, range.endOffset)
+                );
+            }
+            return positions;
+        }
+
+        function updateRangesFromBoundaries(ranges, positions) {
+            for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) {
+                range = ranges[i];
+                start = positions[i * 2];
+                end = positions[i * 2 + 1];
+                range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
+            }
+        }
+
+        function isSplitPoint(node, offset) {
+            if (dom.isCharacterDataNode(node)) {
+                if (offset == 0) {
+                    return !!node.previousSibling;
+                } else if (offset == node.length) {
+                    return !!node.nextSibling;
+                } else {
+                    return true;
+                }
+            }
+
+            return offset > 0 && offset < node.childNodes.length;
+        }
+
+        function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) {
+            var newNode, parentNode;
+            var splitAtStart = (descendantOffset == 0);
+
+            if (dom.isAncestorOf(descendantNode, node)) {
+                return node;
+            }
+
+            if (dom.isCharacterDataNode(descendantNode)) {
+                var descendantIndex = dom.getNodeIndex(descendantNode);
+                if (descendantOffset == 0) {
+                    descendantOffset = descendantIndex;
+                } else if (descendantOffset == descendantNode.length) {
+                    descendantOffset = descendantIndex + 1;
+                } else {
+                    throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" +
+                        descendantOffset + " in " + descendantNode.data);
+                }
+                descendantNode = descendantNode.parentNode;
+            }
+
+            if (isSplitPoint(descendantNode, descendantOffset)) {
+                // descendantNode is now guaranteed not to be a text or other character node
+                newNode = descendantNode.cloneNode(false);
+                parentNode = descendantNode.parentNode;
+                if (newNode.id) {
+                    newNode.removeAttribute("id");
+                }
+                var child, newChildIndex = 0;
+
+                while ( (child = descendantNode.childNodes[descendantOffset]) ) {
+                    movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve);
+                }
+                movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve);
+                return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve);
+            } else if (node != descendantNode) {
+                newNode = descendantNode.parentNode;
+
+                // Work out a new split point in the parent node
+                var newNodeIndex = dom.getNodeIndex(descendantNode);
+
+                if (!splitAtStart) {
+                    newNodeIndex++;
+                }
+                return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve);
+            }
+            return node;
+        }
+
+        function areElementsMergeable(el1, el2) {
+            return el1.namespaceURI == el2.namespaceURI &&
+                el1.tagName.toLowerCase() == el2.tagName.toLowerCase() &&
+                haveSameClasses(el1, el2) &&
+                elementsHaveSameNonClassAttributes(el1, el2) &&
+                getComputedStyleProperty(el1, "display") == "inline" &&
+                getComputedStyleProperty(el2, "display") == "inline";
+        }
+
+        function createAdjacentMergeableTextNodeGetter(forward) {
+            var siblingPropName = forward ? "nextSibling" : "previousSibling";
+
+            return function(textNode, checkParentElement) {
+                var el = textNode.parentNode;
+                var adjacentNode = textNode[siblingPropName];
+                if (adjacentNode) {
+                    // Can merge if the node's previous/next sibling is a text node
+                    if (adjacentNode && adjacentNode.nodeType == 3) {
+                        return adjacentNode;
+                    }
+                } else if (checkParentElement) {
+                    // Compare text node parent element with its sibling
+                    adjacentNode = el[siblingPropName];
+                    if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
+                        var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"];
+                        if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) {
+                            return adjacentNodeChild;
+                        }
+                    }
+                }
+                return null;
+            };
+        }
+
+        var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
+            getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
+
+    
+        function Merge(firstNode) {
+            this.isElementMerge = (firstNode.nodeType == 1);
+            this.textNodes = [];
+            var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
+            if (firstTextNode) {
+                this.textNodes[0] = firstTextNode;
+            }
+        }
+
+        Merge.prototype = {
+            doMerge: function(positionsToPreserve) {
+                var textNodes = this.textNodes;
+                var firstTextNode = textNodes[0];
+                if (textNodes.length > 1) {
+                    var firstTextNodeIndex = dom.getNodeIndex(firstTextNode);
+                    var textParts = [], combinedTextLength = 0, textNode, parent;
+                    forEach(textNodes, function(textNode, i) {
+                        parent = textNode.parentNode;
+                        if (i > 0) {
+                            parent.removeChild(textNode);
+                            if (!parent.hasChildNodes()) {
+                                dom.removeNode(parent);
+                            }
+                            if (positionsToPreserve) {
+                                forEach(positionsToPreserve, function(position) {
+                                    // Handle case where position is inside the text node being merged into a preceding node
+                                    if (position.node == textNode) {
+                                        position.node = firstTextNode;
+                                        position.offset += combinedTextLength;
+                                    }
+                                    // Handle case where both text nodes precede the position within the same parent node
+                                    if (position.node == parent && position.offset > firstTextNodeIndex) {
+                                        --position.offset;
+                                        if (position.offset == firstTextNodeIndex + 1 && i < len - 1) {
+                                            position.node = firstTextNode;
+                                            position.offset = combinedTextLength;
+                                        }
+                                    }
+                                });
+                            }
+                        }
+                        textParts[i] = textNode.data;
+                        combinedTextLength += textNode.data.length;
+                    });
+                    firstTextNode.data = textParts.join("");
+                }
+                return firstTextNode.data;
+            },
+
+            getLength: function() {
+                var i = this.textNodes.length, len = 0;
+                while (i--) {
+                    len += this.textNodes[i].length;
+                }
+                return len;
+            },
+
+            toString: function() {
+                var textParts = [];
+                forEach(this.textNodes, function(textNode, i) {
+                    textParts[i] = "'" + textNode.data + "'";
+                });
+                return "[Merge(" + textParts.join(",") + ")]";
+            }
+        };
+
+        var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements",
+            "removeEmptyElements", "onElementCreate"];
+
+        // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really??
+        var attrNamesForProperties = {};
+
+        function ClassApplier(className, options, tagNames) {
+            var normalize, i, len, propName, applier = this;
+            applier.cssClass = applier.className = className; // cssClass property is for backward compatibility
+
+            var elementPropertiesFromOptions = null, elementAttributes = {};
+
+            // Initialize from options object
+            if (typeof options == "object" && options !== null) {
+                if (typeof options.elementTagName !== "undefined") {
+                    options.elementTagName = options.elementTagName.toLowerCase();
+                }
+                tagNames = options.tagNames;
+                elementPropertiesFromOptions = options.elementProperties;
+                elementAttributes = options.elementAttributes;
+
+                for (i = 0; propName = optionProperties[i++]; ) {
+                    if (options.hasOwnProperty(propName)) {
+                        applier[propName] = options[propName];
+                    }
+                }
+                normalize = options.normalize;
+            } else {
+                normalize = options;
+            }
+
+            // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying
+            applier.normalize = (typeof normalize == "undefined") ? true : normalize;
+
+            // Initialize element properties and attribute exceptions
+            applier.attrExceptions = [];
+            var el = document.createElement(applier.elementTagName);
+            applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true);
+            each(elementAttributes, function(attrName, attrValue) {
+                applier.attrExceptions.push(attrName);
+                // Ensure each attribute value is a string
+                elementAttributes[attrName] = "" + attrValue;
+            });
+            applier.elementAttributes = elementAttributes;
+
+            applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ?
+                sortClassName(applier.elementProperties.className + " " + className) : className;
+
+            // Initialize tag names
+            applier.applyToAnyTagName = false;
+            var type = typeof tagNames;
+            if (type == "string") {
+                if (tagNames == "*") {
+                    applier.applyToAnyTagName = true;
+                } else {
+                    applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
+                }
+            } else if (type == "object" && typeof tagNames.length == "number") {
+                applier.tagNames = [];
+                for (i = 0, len = tagNames.length; i < len; ++i) {
+                    if (tagNames[i] == "*") {
+                        applier.applyToAnyTagName = true;
+                    } else {
+                        applier.tagNames.push(tagNames[i].toLowerCase());
+                    }
+                }
+            } else {
+                applier.tagNames = [applier.elementTagName];
+            }
+        }
+
+        ClassApplier.prototype = {
+            elementTagName: defaultTagName,
+            elementProperties: {},
+            elementAttributes: {},
+            ignoreWhiteSpace: true,
+            applyToEditableOnly: false,
+            useExistingElements: true,
+            removeEmptyElements: true,
+            onElementCreate: null,
+
+            copyPropertiesToElement: function(props, el, createCopy) {
+                var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName;
+
+                for (var p in props) {
+                    if (props.hasOwnProperty(p)) {
+                        propValue = props[p];
+                        elPropValue = el[p];
+
+                        // Special case for class. The copied properties object has the applier's class as well as its own
+                        // to simplify checks when removing styling elements
+                        if (p == "className") {
+                            addClass(el, propValue);
+                            addClass(el, this.className);
+                            el[p] = sortClassName(el[p]);
+                            if (createCopy) {
+                                elProps[p] = propValue;
+                            }
+                        }
+
+                        // Special case for style
+                        else if (p == "style") {
+                            elStyle = elPropValue;
+                            if (createCopy) {
+                                elProps[p] = elPropsStyle = {};
+                            }
+                            for (s in props[p]) {
+                                if (props[p].hasOwnProperty(s)) {
+                                    elStyle[s] = propValue[s];
+                                    if (createCopy) {
+                                        elPropsStyle[s] = elStyle[s];
+                                    }
+                                }
+                            }
+                            this.attrExceptions.push(p);
+                        } else {
+                            el[p] = propValue;
+                            // Copy the property back from the dummy element so that later comparisons to check whether
+                            // elements may be removed are checking against the right value. For example, the href property
+                            // of an element returns a fully qualified URL even if it was previously assigned a relative
+                            // URL.
+                            if (createCopy) {
+                                elProps[p] = el[p];
+
+                                // Not all properties map to identically-named attributes
+                                attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p;
+                                this.attrExceptions.push(attrName);
+                            }
+                        }
+                    }
+                }
+
+                return createCopy ? elProps : "";
+            },
+
+            copyAttributesToElement: function(attrs, el) {
+                for (var attrName in attrs) {
+                    if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) {
+                        el.setAttribute(attrName, attrs[attrName]);
+                    }
+                }
+            },
+
+            appliesToElement: function(el) {
+                return contains(this.tagNames, el.tagName.toLowerCase());
+            },
+
+            getEmptyElements: function(range) {
+                var applier = this;
+                return range.getNodes([1], function(el) {
+                    return applier.appliesToElement(el) && !el.hasChildNodes();
+                });
+            },
+
+            hasClass: function(node) {
+                return node.nodeType == 1 &&
+                    (this.applyToAnyTagName || this.appliesToElement(node)) &&
+                    hasClass(node, this.className);
+            },
+
+            getSelfOrAncestorWithClass: function(node) {
+                while (node) {
+                    if (this.hasClass(node)) {
+                        return node;
+                    }
+                    node = node.parentNode;
+                }
+                return null;
+            },
+
+            isModifiable: function(node) {
+                return !this.applyToEditableOnly || isEditable(node);
+            },
+
+            // White space adjacent to an unwrappable node can be ignored for wrapping
+            isIgnorableWhiteSpaceNode: function(node) {
+                return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
+            },
+
+            // Normalizes nodes after applying a class to a Range.
+            postApply: function(textNodes, range, positionsToPreserve, isUndo) {
+                var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
+
+                var merges = [], currentMerge;
+
+                var rangeStartNode = firstNode, rangeEndNode = lastNode;
+                var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
+
+                var textNode, precedingTextNode;
+
+                // Check for every required merge and create a Merge object for each
+                forEach(textNodes, function(textNode) {
+                    precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
+                    if (precedingTextNode) {
+                        if (!currentMerge) {
+                            currentMerge = new Merge(precedingTextNode);
+                            merges.push(currentMerge);
+                        }
+                        currentMerge.textNodes.push(textNode);
+                        if (textNode === firstNode) {
+                            rangeStartNode = currentMerge.textNodes[0];
+                            rangeStartOffset = rangeStartNode.length;
+                        }
+                        if (textNode === lastNode) {
+                            rangeEndNode = currentMerge.textNodes[0];
+                            rangeEndOffset = currentMerge.getLength();
+                        }
+                    } else {
+                        currentMerge = null;
+                    }
+                });
+
+                // Test whether the first node after the range needs merging
+                var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
+
+                if (nextTextNode) {
+                    if (!currentMerge) {
+                        currentMerge = new Merge(lastNode);
+                        merges.push(currentMerge);
+                    }
+                    currentMerge.textNodes.push(nextTextNode);
+                }
+
+                // Apply the merges
+                if (merges.length) {
+                    for (i = 0, len = merges.length; i < len; ++i) {
+                        merges[i].doMerge(positionsToPreserve);
+                    }
+
+                    // Set the range boundaries
+                    range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
+                }
+            },
+
+            createContainer: function(parentNode,serializedHighlight) {
+                var doc = dom.getDocument(parentNode);
+                var namespace;
+                var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ?
+                    doc.createElementNS(parentNode.namespaceURI, this.elementTagName) :
+                    doc.createElement(this.elementTagName);
+
+                this.copyPropertiesToElement(this.elementProperties, el, false);
+                this.copyAttributesToElement(this.elementAttributes, el);
+                 //Highlight.printData("createContainer");
+                addClass(el, this.className, serializedHighlight);
+                if (this.onElementCreate) {
+                    this.onElementCreate(el, this);
+                }
+                return el;
+            },
+
+            elementHasProperties: function(el, props) {
+                var applier = this;
+                return each(props, function(p, propValue) {
+                    if (p == "className") {
+                        // For checking whether we should reuse an existing element, we just want to check that the element
+                        // has all the classes specified in the className property. When deciding whether the element is
+                        // removable when unapplying a class, there is separate special handling to check whether the
+                        // element has extra classes so the same simple check will do.
+                        return hasAllClasses(el, propValue);
+                    } else if (typeof propValue == "object") {
+                        if (!applier.elementHasProperties(el[p], propValue)) {
+                            return false;
+                        }
+                    } else if (el[p] !== propValue) {
+                        return false;
+                    }
+                });
+            },
+
+            elementHasAttributes: function(el, attrs) {
+                return each(attrs, function(name, value) {
+                    if (el.getAttribute(name) !== value) {
+                        return false;
+                    }
+                });
+            },
+
+            applyToTextNode: function(textNode, positionsToPreserve,serializedHighlight) {
+
+                // Check whether the text node can be styled. Text within a <style> or <script> element, for example,
+                // should not be styled. See issue 283.
+                if (canTextBeStyled(textNode)) {
+                    var parent = textNode.parentNode;
+                    if (parent.childNodes.length == 1 &&
+                        this.useExistingElements &&
+                        this.appliesToElement(parent) &&
+                        this.elementHasProperties(parent, this.elementProperties) &&
+                        this.elementHasAttributes(parent, this.elementAttributes)) {
+                        addClass(parent, this.className, serializedHighlight);
+                    } else {
+                        var textNodeParent = textNode.parentNode;
+                        var el = this.createContainer(textNodeParent, serializedHighlight);
+                        textNodeParent.insertBefore(el, textNode);
+                        el.appendChild(textNode);
+                    }
+                }
+
+            },
+
+            isRemovable: function(el) {
+                return el.tagName.toLowerCase() == this.elementTagName &&
+                    getSortedClassName(el) == this.elementSortedClassName &&
+                    this.elementHasProperties(el, this.elementProperties) &&
+                    !elementHasNonClassAttributes(el, this.attrExceptions) &&
+                    this.elementHasAttributes(el, this.elementAttributes) &&
+                    this.isModifiable(el);
+            },
+
+            isEmptyContainer: function(el) {
+                var childNodeCount = el.childNodes.length;
+                return el.nodeType == 1 &&
+                    this.isRemovable(el) &&
+                    (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild)));
+            },
+
+            removeEmptyContainers: function(range) {
+                var applier = this;
+                var nodesToRemove = range.getNodes([1], function(el) {
+                    return applier.isEmptyContainer(el);
+                });
+
+                var rangesToPreserve = [range];
+                var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
+
+                forEach(nodesToRemove, function(node) {
+                    removePreservingPositions(node, positionsToPreserve);
+                });
+
+                // Update the range from the preserved boundary positions
+                updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
+            },
+
+            undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) {
+                if (!range.containsNode(ancestorWithClass)) {
+                    // Split out the portion of the ancestor from which we can remove the class
+                    //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
+                    var ancestorRange = range.cloneRange();
+                    ancestorRange.selectNode(ancestorWithClass);
+                    if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) {
+                        splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve);
+                        range.setEndAfter(ancestorWithClass);
+                    }
+                    if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) {
+                        ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve);
+                    }
+                }
+
+                if (this.isRemovable(ancestorWithClass)) {
+                    replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
+                } else {
+                    removeClass(ancestorWithClass, this.className);
+                }
+            },
+
+            splitAncestorWithClass: function(container, offset, positionsToPreserve) {
+                var ancestorWithClass = this.getSelfOrAncestorWithClass(container);
+                if (ancestorWithClass) {
+                    splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve);
+                }
+            },
+
+            undoToAncestor: function(ancestorWithClass, positionsToPreserve) {
+                if (this.isRemovable(ancestorWithClass)) {
+                    replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
+                } else {
+                    removeClass(ancestorWithClass, this.className);
+                }
+            },
+
+            applyToRange: function(range, rangesToPreserve, serializedHighlight) {
+                var applier = this;
+                rangesToPreserve = rangesToPreserve || [];
+
+                // Create an array of range boundaries to preserve
+                var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []);
+
+                range.splitBoundariesPreservingPositions(positionsToPreserve);
+
+                // Tidy up the DOM by removing empty containers
+                if (applier.removeEmptyElements) {
+                    applier.removeEmptyContainers(range);
+                }
+
+                var textNodes = getEffectiveTextNodes(range);
+
+                if (textNodes.length) {
+                    forEach(textNodes, function(textNode) {
+                        if (!applier.isIgnorableWhiteSpaceNode(textNode) && !applier.getSelfOrAncestorWithClass(textNode) &&
+                                applier.isModifiable(textNode)) {
+
+                                applier.applyToTextNode(textNode, positionsToPreserve,serializedHighlight);
+                        }
+                    });
+                    var lastTextNode = textNodes[textNodes.length - 1];
+                    range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
+                    if (applier.normalize) {
+                        applier.postApply(textNodes, range, positionsToPreserve, false);
+                    }
+
+                    // Update the ranges from the preserved boundary positions
+                    updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
+                }
+
+                // Apply classes to any appropriate empty elements
+                var emptyElements = applier.getEmptyElements(range);
+
+                forEach(emptyElements, function(el) {
+                    addClass(el, applier.className);
+                });
+            },
+
+            applyToRanges: function(ranges) {
+
+                var i = ranges.length;
+                while (i--) {
+                    this.applyToRange(ranges[i], ranges);
+                    //Highlight.printData("**ranges"+ranges[i]);
+                }
+
+
+                return ranges;
+            },
+
+            applyToSelection: function(win) {
+                var sel = api.getSelection(win);
+                sel.setRanges( this.applyToRanges(sel.getAllRanges()) );
+            },
+
+            undoToRange: function(range, rangesToPreserve) {
+                var applier = this;
+                // Create an array of range boundaries to preserve
+                rangesToPreserve = rangesToPreserve || [];
+                var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
+
+
+                range.splitBoundariesPreservingPositions(positionsToPreserve);
+
+                // Tidy up the DOM by removing empty containers
+                if (applier.removeEmptyElements) {
+                    applier.removeEmptyContainers(range, positionsToPreserve);
+                }
+
+                var textNodes = getEffectiveTextNodes(range);
+                var textNode, ancestorWithClass;
+                var lastTextNode = textNodes[textNodes.length - 1];
+
+                if (textNodes.length) {
+                    applier.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve);
+                    applier.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve);
+                    for (var i = 0, len = textNodes.length; i < len; ++i) {
+                        textNode = textNodes[i];
+                        ancestorWithClass = applier.getSelfOrAncestorWithClass(textNode);
+                        if (ancestorWithClass && applier.isModifiable(textNode)) {
+                            applier.undoToAncestor(ancestorWithClass, positionsToPreserve);
+                        }
+                    }
+                    // Ensure the range is still valid
+                    range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
+
+
+                    if (applier.normalize) {
+                        applier.postApply(textNodes, range, positionsToPreserve, true);
+                    }
+
+                    // Update the ranges from the preserved boundary positions
+                    updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
+                }
+
+                // Remove class from any appropriate empty elements
+                var emptyElements = applier.getEmptyElements(range);
+
+                forEach(emptyElements, function(el) {
+                    removeClass(el, applier.className);
+                });
+            },
+
+            undoToRanges: function(ranges) {
+                // Get ranges returned in document order
+                var i = ranges.length;
+
+                while (i--) {
+                    this.undoToRange(ranges[i], ranges);
+                }
+
+                return ranges;
+            },
+
+            undoToSelection: function(win) {
+                var sel = api.getSelection(win);
+                var ranges = api.getSelection(win).getAllRanges();
+                this.undoToRanges(ranges);
+                sel.setRanges(ranges);
+            },
+
+            isAppliedToRange: function(range) {
+                if (range.collapsed || range.toString() == "") {
+                    return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
+                } else {
+                    var textNodes = range.getNodes( [3] );
+                    if (textNodes.length)
+                    for (var i = 0, textNode; textNode = textNodes[i++]; ) {
+                        if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) &&
+                                this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
+                            return false;
+                        }
+                    }
+                    return true;
+                }
+            },
+
+            isAppliedToRanges: function(ranges) {
+                var i = ranges.length;
+                if (i == 0) {
+                    return false;
+                }
+                while (i--) {
+                    if (!this.isAppliedToRange(ranges[i])) {
+                        return false;
+                    }
+                }
+                return true;
+            },
+
+            isAppliedToSelection: function(win) {
+                var sel = api.getSelection(win);
+                return this.isAppliedToRanges(sel.getAllRanges());
+            },
+
+            toggleRange: function(range) {
+                if (this.isAppliedToRange(range)) {
+                    this.undoToRange(range);
+                } else {
+                    this.applyToRange(range);
+                    //Highlight.printData("**toggleRange"+ranges[i]);
+                }
+            },
+
+            toggleSelection: function(win) {
+                if (this.isAppliedToSelection(win)) {
+                    this.undoToSelection(win);
+                } else {
+                    this.applyToSelection(win);
+                }
+            },
+
+            getElementsWithClassIntersectingRange: function(range) {
+                var elements = [];
+                var applier = this;
+                range.getNodes([3], function(textNode) {
+                    var el = applier.getSelfOrAncestorWithClass(textNode);
+                    if (el && !contains(elements, el)) {
+                        elements.push(el);
+                    }
+                });
+                return elements;
+            },
+
+            detach: function() {}
+        };
+
+        function createClassApplier(className, options, tagNames) {
+            return new ClassApplier(className, options, tagNames);
+        }
+
+        ClassApplier.util = {
+            hasClass: hasClass,
+            addClass: addClass,
+            removeClass: removeClass,
+            getClass: getClass,
+            hasSameClasses: haveSameClasses,
+            hasAllClasses: hasAllClasses,
+            replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions,
+            elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
+            elementHasNonClassAttributes: elementHasNonClassAttributes,
+            splitNodeAt: splitNodeAt,
+            isEditableElement: isEditableElement,
+            isEditingHost: isEditingHost,
+            isEditable: isEditable
+        };
+
+        api.CssClassApplier = api.ClassApplier = ClassApplier;
+        api.createClassApplier = createClassApplier;
+        util.createAliasForDeprecatedMethod(api, "createCssClassApplier", "createClassApplier", module);
+    });
+    
+    return rangy;
+}, this);
diff --git a/Android/folioreader/src/main/assets/js/rangy-core.js b/Android/folioreader/src/main/assets/js/rangy-core.js
new file mode 100755 (executable)
index 0000000..69e95bf
--- /dev/null
@@ -0,0 +1,3845 @@
+/**
+ * Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+
+(function(factory, root) {
+    if (typeof define == "function" && define.amd) {
+        // AMD. Register as an anonymous module.
+        define(factory);
+    } else if (typeof module != "undefined" && typeof exports == "object") {
+        // Node/CommonJS style
+        module.exports = factory();
+    } else {
+        // No AMD or CommonJS support so we place Rangy in (probably) the global variable
+        root.rangy = factory();
+    }
+})(function() {
+
+    var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
+
+    // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
+    // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
+    var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
+        "commonAncestorContainer"];
+
+    // Minimal set of methods required for DOM Level 2 Range compliance
+    var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
+        "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
+        "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
+
+    var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
+
+    // Subset of TextRange's full set of methods that we're interested in
+    var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
+        "setEndPoint", "getBoundingClientRect"];
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Trio of functions taken from Peter Michaux's article:
+    // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
+    function isHostMethod(o, p) {
+        var t = typeof o[p];
+        return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
+    }
+
+    function isHostObject(o, p) {
+        return !!(typeof o[p] == OBJECT && o[p]);
+    }
+
+    function isHostProperty(o, p) {
+        return typeof o[p] != UNDEFINED;
+    }
+
+    // Creates a convenience function to save verbose repeated calls to tests functions
+    function createMultiplePropertyTest(testFunc) {
+        return function(o, props) {
+            var i = props.length;
+            while (i--) {
+                if (!testFunc(o, props[i])) {
+                    return false;
+                }
+            }
+            return true;
+        };
+    }
+
+    // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
+    var areHostMethods = createMultiplePropertyTest(isHostMethod);
+    var areHostObjects = createMultiplePropertyTest(isHostObject);
+    var areHostProperties = createMultiplePropertyTest(isHostProperty);
+
+    function isTextRange(range) {
+        return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
+    }
+
+    function getBody(doc) {
+        return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
+    }
+
+    var forEach = [].forEach ?
+        function(arr, func) {
+            arr.forEach(func);
+        } :
+        function(arr, func) {
+            for (var i = 0, len = arr.length; i < len; ++i) {
+                func(arr[i], i);
+            }
+        };
+
+    var modules = {};
+
+    var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
+
+    var util = {
+        isHostMethod: isHostMethod,
+        isHostObject: isHostObject,
+        isHostProperty: isHostProperty,
+        areHostMethods: areHostMethods,
+        areHostObjects: areHostObjects,
+        areHostProperties: areHostProperties,
+        isTextRange: isTextRange,
+        getBody: getBody,
+        forEach: forEach
+    };
+
+    var api = {
+        version: "1.3.0",
+        initialized: false,
+        isBrowser: isBrowser,
+        supported: true,
+        util: util,
+        features: {},
+        modules: modules,
+        config: {
+            alertOnFail: false,
+            alertOnWarn: false,
+            preferTextRange: false,
+            autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
+        }
+    };
+
+    function consoleLog(msg) {
+        if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
+            console.log(msg);
+        }
+    }
+
+    function alertOrLog(msg, shouldAlert) {
+        if (isBrowser && shouldAlert) {
+            alert(msg);
+        } else  {
+            consoleLog(msg);
+        }
+    }
+
+    function fail(reason) {
+        api.initialized = true;
+        api.supported = false;
+        alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
+    }
+
+    api.fail = fail;
+
+    function warn(msg) {
+        alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
+    }
+
+    api.warn = warn;
+
+    // Add utility extend() method
+    var extend;
+    if ({}.hasOwnProperty) {
+        util.extend = extend = function(obj, props, deep) {
+            var o, p;
+            for (var i in props) {
+                if (props.hasOwnProperty(i)) {
+                    o = obj[i];
+                    p = props[i];
+                    if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
+                        extend(o, p, true);
+                    }
+                    obj[i] = p;
+                }
+            }
+            // Special case for toString, which does not show up in for...in loops in IE <= 8
+            if (props.hasOwnProperty("toString")) {
+                obj.toString = props.toString;
+            }
+            return obj;
+        };
+
+        util.createOptions = function(optionsParam, defaults) {
+            var options = {};
+            extend(options, defaults);
+            if (optionsParam) {
+                extend(options, optionsParam);
+            }
+            return options;
+        };
+    } else {
+        fail("hasOwnProperty not supported");
+    }
+
+    // Test whether we're in a browser and bail out if not
+    if (!isBrowser) {
+        fail("Rangy can only run in a browser");
+    }
+
+    // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
+    (function() {
+        var toArray;
+
+        if (isBrowser) {
+            var el = document.createElement("div");
+            el.appendChild(document.createElement("span"));
+            var slice = [].slice;
+            try {
+                if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
+                    toArray = function(arrayLike) {
+                        return slice.call(arrayLike, 0);
+                    };
+                }
+            } catch (e) {}
+        }
+
+        if (!toArray) {
+            toArray = function(arrayLike) {
+                var arr = [];
+                for (var i = 0, len = arrayLike.length; i < len; ++i) {
+                    arr[i] = arrayLike[i];
+                }
+                return arr;
+            };
+        }
+
+        util.toArray = toArray;
+    })();
+
+    // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
+    // normalization of event properties
+    var addListener;
+    if (isBrowser) {
+        if (isHostMethod(document, "addEventListener")) {
+            addListener = function(obj, eventType, listener) {
+                obj.addEventListener(eventType, listener, false);
+            };
+        } else if (isHostMethod(document, "attachEvent")) {
+            addListener = function(obj, eventType, listener) {
+                obj.attachEvent("on" + eventType, listener);
+            };
+        } else {
+            fail("Document does not have required addEventListener or attachEvent method");
+        }
+
+        util.addListener = addListener;
+    }
+
+    var initListeners = [];
+
+    function getErrorDesc(ex) {
+        return ex.message || ex.description || String(ex);
+    }
+
+    // Initialization
+    function init() {
+        if (!isBrowser || api.initialized) {
+            return;
+        }
+        var testRange;
+        var implementsDomRange = false, implementsTextRange = false;
+
+        // First, perform basic feature tests
+
+        if (isHostMethod(document, "createRange")) {
+            testRange = document.createRange();
+            if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
+                implementsDomRange = true;
+            }
+        }
+
+        var body = getBody(document);
+        if (!body || body.nodeName.toLowerCase() != "body") {
+            fail("No body element found");
+            return;
+        }
+
+        if (body && isHostMethod(body, "createTextRange")) {
+            testRange = body.createTextRange();
+            if (isTextRange(testRange)) {
+                implementsTextRange = true;
+            }
+        }
+
+        if (!implementsDomRange && !implementsTextRange) {
+            fail("Neither Range nor TextRange are available");
+            return;
+        }
+
+        api.initialized = true;
+        api.features = {
+            implementsDomRange: implementsDomRange,
+            implementsTextRange: implementsTextRange
+        };
+
+        // Initialize modules
+        var module, errorMessage;
+        for (var moduleName in modules) {
+            if ( (module = modules[moduleName]) instanceof Module ) {
+                module.init(module, api);
+            }
+        }
+
+        // Call init listeners
+        for (var i = 0, len = initListeners.length; i < len; ++i) {
+            try {
+                initListeners[i](api);
+            } catch (ex) {
+                errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
+                consoleLog(errorMessage);
+            }
+        }
+    }
+
+    function deprecationNotice(deprecated, replacement, module) {
+        if (module) {
+            deprecated += " in module " + module.name;
+        }
+        api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
+        replacement + " instead.");
+    }
+
+    function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
+        owner[deprecated] = function() {
+            deprecationNotice(deprecated, replacement, module);
+            return owner[replacement].apply(owner, util.toArray(arguments));
+        };
+    }
+
+    util.deprecationNotice = deprecationNotice;
+    util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
+
+    // Allow external scripts to initialize this library in case it's loaded after the document has loaded
+    api.init = init;
+
+    // Execute listener immediately if already initialized
+    api.addInitListener = function(listener) {
+        if (api.initialized) {
+            listener(api);
+        } else {
+            initListeners.push(listener);
+        }
+    };
+
+    var shimListeners = [];
+
+    api.addShimListener = function(listener) {
+        shimListeners.push(listener);
+    };
+
+    function shim(win) {
+        win = win || window;
+        init();
+
+        // Notify listeners
+        for (var i = 0, len = shimListeners.length; i < len; ++i) {
+            shimListeners[i](win);
+        }
+    }
+
+    if (isBrowser) {
+        api.shim = api.createMissingNativeApi = shim;
+        createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
+    }
+
+    function Module(name, dependencies, initializer) {
+        this.name = name;
+        this.dependencies = dependencies;
+        this.initialized = false;
+        this.supported = false;
+        this.initializer = initializer;
+    }
+
+    Module.prototype = {
+        init: function() {
+            var requiredModuleNames = this.dependencies || [];
+            for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
+                moduleName = requiredModuleNames[i];
+
+                requiredModule = modules[moduleName];
+                if (!requiredModule || !(requiredModule instanceof Module)) {
+                    throw new Error("required module '" + moduleName + "' not found");
+                }
+
+                requiredModule.init();
+
+                if (!requiredModule.supported) {
+                    throw new Error("required module '" + moduleName + "' not supported");
+                }
+            }
+
+            // Now run initializer
+            this.initializer(this);
+        },
+
+        fail: function(reason) {
+            this.initialized = true;
+            this.supported = false;
+            throw new Error(reason);
+        },
+
+        warn: function(msg) {
+            api.warn("Module " + this.name + ": " + msg);
+        },
+
+        deprecationNotice: function(deprecated, replacement) {
+            api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
+                replacement + " instead");
+        },
+
+        createError: function(msg) {
+            return new Error("Error in Rangy " + this.name + " module: " + msg);
+        }
+    };
+
+    function createModule(name, dependencies, initFunc) {
+        var newModule = new Module(name, dependencies, function(module) {
+            if (!module.initialized) {
+                module.initialized = true;
+                try {
+                    initFunc(api, module);
+                    module.supported = true;
+                } catch (ex) {
+                    var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
+                    consoleLog(errorMessage);
+                    if (ex.stack) {
+                        consoleLog(ex.stack);
+                    }
+                }
+            }
+        });
+        modules[name] = newModule;
+        return newModule;
+    }
+
+    api.createModule = function(name) {
+        // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
+        var initFunc, dependencies;
+        if (arguments.length == 2) {
+            initFunc = arguments[1];
+            dependencies = [];
+        } else {
+            initFunc = arguments[2];
+            dependencies = arguments[1];
+        }
+
+        var module = createModule(name, dependencies, initFunc);
+
+        // Initialize the module immediately if the core is already initialized
+        if (api.initialized && api.supported) {
+            module.init();
+        }
+    };
+
+    api.createCoreModule = function(name, dependencies, initFunc) {
+        createModule(name, dependencies, initFunc);
+    };
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
+
+    function RangePrototype() {}
+    api.RangePrototype = RangePrototype;
+    api.rangePrototype = new RangePrototype();
+
+    function SelectionPrototype() {}
+    api.selectionPrototype = new SelectionPrototype();
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // DOM utility methods used by Rangy
+    api.createCoreModule("DomUtil", [], function(api, module) {
+        var UNDEF = "undefined";
+        var util = api.util;
+        var getBody = util.getBody;
+
+        // Perform feature tests
+        if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
+            module.fail("document missing a Node creation method");
+        }
+
+        if (!util.isHostMethod(document, "getElementsByTagName")) {
+            module.fail("document missing getElementsByTagName method");
+        }
+
+        var el = document.createElement("div");
+        if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
+                !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
+            module.fail("Incomplete Element implementation");
+        }
+
+        // innerHTML is required for Range's createContextualFragment method
+        if (!util.isHostProperty(el, "innerHTML")) {
+            module.fail("Element is missing innerHTML property");
+        }
+
+        var textNode = document.createTextNode("test");
+        if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
+                !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
+                !util.areHostProperties(textNode, ["data"]))) {
+            module.fail("Incomplete Text Node implementation");
+        }
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
+        // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
+        // contains just the document as a single element and the value searched for is the document.
+        var arrayContains = /*Array.prototype.indexOf ?
+            function(arr, val) {
+                return arr.indexOf(val) > -1;
+            }:*/
+
+            function(arr, val) {
+                var i = arr.length;
+                while (i--) {
+                    if (arr[i] === val) {
+                        return true;
+                    }
+                }
+                return false;
+            };
+
+        // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
+        function isHtmlNamespace(node) {
+            var ns;
+            return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
+        }
+
+        function parentElement(node) {
+            var parent = node.parentNode;
+            return (parent.nodeType == 1) ? parent : null;
+        }
+
+        function getNodeIndex(node) {
+            var i = 0;
+            while( (node = node.previousSibling) ) {
+                ++i;
+            }
+            return i;
+        }
+
+        function getNodeLength(node) {
+            switch (node.nodeType) {
+                case 7:
+                case 10:
+                    return 0;
+                case 3:
+                case 8:
+                    return node.length;
+                default:
+                    return node.childNodes.length;
+            }
+        }
+
+        function getCommonAncestor(node1, node2) {
+            var ancestors = [], n;
+            for (n = node1; n; n = n.parentNode) {
+                ancestors.push(n);
+            }
+
+            for (n = node2; n; n = n.parentNode) {
+                if (arrayContains(ancestors, n)) {
+                    return n;
+                }
+            }
+
+            return null;
+        }
+
+        function isAncestorOf(ancestor, descendant, selfIsAncestor) {
+            var n = selfIsAncestor ? descendant : descendant.parentNode;
+            while (n) {
+                if (n === ancestor) {
+                    return true;
+                } else {
+                    n = n.parentNode;
+                }
+            }
+            return false;
+        }
+
+        function isOrIsAncestorOf(ancestor, descendant) {
+            return isAncestorOf(ancestor, descendant, true);
+        }
+
+        function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
+            var p, n = selfIsAncestor ? node : node.parentNode;
+            while (n) {
+                p = n.parentNode;
+                if (p === ancestor) {
+                    return n;
+                }
+                n = p;
+            }
+            return null;
+        }
+
+        function isCharacterDataNode(node) {
+            var t = node.nodeType;
+            return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
+        }
+
+        function isTextOrCommentNode(node) {
+            if (!node) {
+                return false;
+            }
+            var t = node.nodeType;
+            return t == 3 || t == 8 ; // Text or Comment
+        }
+
+        function insertAfter(node, precedingNode) {
+            var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
+            if (nextNode) {
+                parent.insertBefore(node, nextNode);
+            } else {
+                parent.appendChild(node);
+            }
+            return node;
+        }
+
+        // Note that we cannot use splitText() because it is bugridden in IE 9.
+        function splitDataNode(node, index, positionsToPreserve) {
+            var newNode = node.cloneNode(false);
+            newNode.deleteData(0, index);
+            node.deleteData(index, node.length - index);
+            insertAfter(newNode, node);
+
+            // Preserve positions
+            if (positionsToPreserve) {
+                for (var i = 0, position; position = positionsToPreserve[i++]; ) {
+                    // Handle case where position was inside the portion of node after the split point
+                    if (position.node == node && position.offset > index) {
+                        position.node = newNode;
+                        position.offset -= index;
+                    }
+                    // Handle the case where the position is a node offset within node's parent
+                    else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
+                        ++position.offset;
+                    }
+                }
+            }
+            return newNode;
+        }
+
+        function getDocument(node) {
+            if (node.nodeType == 9) {
+                return node;
+            } else if (typeof node.ownerDocument != UNDEF) {
+                return node.ownerDocument;
+            } else if (typeof node.document != UNDEF) {
+                return node.document;
+            } else if (node.parentNode) {
+                return getDocument(node.parentNode);
+            } else {
+                throw module.createError("getDocument: no document found for node");
+            }
+        }
+
+        function getWindow(node) {
+            var doc = getDocument(node);
+            if (typeof doc.defaultView != UNDEF) {
+                return doc.defaultView;
+            } else if (typeof doc.parentWindow != UNDEF) {
+                return doc.parentWindow;
+            } else {
+                throw module.createError("Cannot get a window object for node");
+            }
+        }
+
+        function getIframeDocument(iframeEl) {
+            if (typeof iframeEl.contentDocument != UNDEF) {
+                return iframeEl.contentDocument;
+            } else if (typeof iframeEl.contentWindow != UNDEF) {
+                return iframeEl.contentWindow.document;
+            } else {
+                throw module.createError("getIframeDocument: No Document object found for iframe element");
+            }
+        }
+
+        function getIframeWindow(iframeEl) {
+            if (typeof iframeEl.contentWindow != UNDEF) {
+                return iframeEl.contentWindow;
+            } else if (typeof iframeEl.contentDocument != UNDEF) {
+                return iframeEl.contentDocument.defaultView;
+            } else {
+                throw module.createError("getIframeWindow: No Window object found for iframe element");
+            }
+        }
+
+        // This looks bad. Is it worth it?
+        function isWindow(obj) {
+            return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
+        }
+
+        function getContentDocument(obj, module, methodName) {
+            var doc;
+
+            if (!obj) {
+                doc = document;
+            }
+
+            // Test if a DOM node has been passed and obtain a document object for it if so
+            else if (util.isHostProperty(obj, "nodeType")) {
+                doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
+                    getIframeDocument(obj) : getDocument(obj);
+            }
+
+            // Test if the doc parameter appears to be a Window object
+            else if (isWindow(obj)) {
+                doc = obj.document;
+            }
+
+            if (!doc) {
+                throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
+            }
+
+            return doc;
+        }
+
+        function getRootContainer(node) {
+            var parent;
+            while ( (parent = node.parentNode) ) {
+                node = parent;
+            }
+            return node;
+        }
+
+        function comparePoints(nodeA, offsetA, nodeB, offsetB) {
+            // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
+            var nodeC, root, childA, childB, n;
+            if (nodeA == nodeB) {
+                // Case 1: nodes are the same
+                return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
+            } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
+                // Case 2: node C (container B or an ancestor) is a child node of A
+                return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
+            } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
+                // Case 3: node C (container A or an ancestor) is a child node of B
+                return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
+            } else {
+                root = getCommonAncestor(nodeA, nodeB);
+                if (!root) {
+                    throw new Error("comparePoints error: nodes have no common ancestor");
+                }
+
+                // Case 4: containers are siblings or descendants of siblings
+                childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
+                childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
+
+                if (childA === childB) {
+                    // This shouldn't be possible
+                    throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
+                } else {
+                    n = root.firstChild;
+                    while (n) {
+                        if (n === childA) {
+                            return -1;
+                        } else if (n === childB) {
+                            return 1;
+                        }
+                        n = n.nextSibling;
+                    }
+                }
+            }
+        }
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
+        var crashyTextNodes = false;
+
+        function isBrokenNode(node) {
+            var n;
+            try {
+                n = node.parentNode;
+                return false;
+            } catch (e) {
+                return true;
+            }
+        }
+
+        (function() {
+            var el = document.createElement("b");
+            el.innerHTML = "1";
+            var textNode = el.firstChild;
+            el.innerHTML = "<br />";
+            crashyTextNodes = isBrokenNode(textNode);
+
+            api.features.crashyTextNodes = crashyTextNodes;
+        })();
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        function inspectNode(node) {
+            if (!node) {
+                return "[No node]";
+            }
+            if (crashyTextNodes && isBrokenNode(node)) {
+                return "[Broken node]";
+            }
+            if (isCharacterDataNode(node)) {
+                return '"' + node.data + '"';
+            }
+            if (node.nodeType == 1) {
+                var idAttr = node.id ? ' id="' + node.id + '"' : "";
+                return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
+            }
+            return node.nodeName;
+        }
+
+        function fragmentFromNodeChildren(node) {
+            var fragment = getDocument(node).createDocumentFragment(), child;
+            while ( (child = node.firstChild) ) {
+                fragment.appendChild(child);
+            }
+            return fragment;
+        }
+
+        var getComputedStyleProperty;
+        if (typeof window.getComputedStyle != UNDEF) {
+            getComputedStyleProperty = function(el, propName) {
+                return getWindow(el).getComputedStyle(el, null)[propName];
+            };
+        } else if (typeof document.documentElement.currentStyle != UNDEF) {
+            getComputedStyleProperty = function(el, propName) {
+                return el.currentStyle ? el.currentStyle[propName] : "";
+            };
+        } else {
+            module.fail("No means of obtaining computed style properties found");
+        }
+
+        function createTestElement(doc, html, contentEditable) {
+            var body = getBody(doc);
+            var el = doc.createElement("div");
+            el.contentEditable = "" + !!contentEditable;
+            if (html) {
+                el.innerHTML = html;
+            }
+
+            // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
+            var bodyFirstChild = body.firstChild;
+            if (bodyFirstChild) {
+                body.insertBefore(el, bodyFirstChild);
+            } else {
+                body.appendChild(el);
+            }
+
+            return el;
+        }
+
+        function removeNode(node) {
+            return node.parentNode.removeChild(node);
+        }
+
+        function NodeIterator(root) {
+            this.root = root;
+            this._next = root;
+        }
+
+        NodeIterator.prototype = {
+            _current: null,
+
+            hasNext: function() {
+                return !!this._next;
+            },
+
+            next: function() {
+                var n = this._current = this._next;
+                var child, next;
+                if (this._current) {
+                    child = n.firstChild;
+                    if (child) {
+                        this._next = child;
+                    } else {
+                        next = null;
+                        while ((n !== this.root) && !(next = n.nextSibling)) {
+                            n = n.parentNode;
+                        }
+                        this._next = next;
+                    }
+                }
+                return this._current;
+            },
+
+            detach: function() {
+                this._current = this._next = this.root = null;
+            }
+        };
+
+        function createIterator(root) {
+            return new NodeIterator(root);
+        }
+
+        function DomPosition(node, offset) {
+            this.node = node;
+            this.offset = offset;
+        }
+
+        DomPosition.prototype = {
+            equals: function(pos) {
+                return !!pos && this.node === pos.node && this.offset == pos.offset;
+            },
+
+            inspect: function() {
+                return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
+            },
+
+            toString: function() {
+                return this.inspect();
+            }
+        };
+
+        function DOMException(codeName) {
+            this.code = this[codeName];
+            this.codeName = codeName;
+            this.message = "DOMException: " + this.codeName;
+        }
+
+        DOMException.prototype = {
+            INDEX_SIZE_ERR: 1,
+            HIERARCHY_REQUEST_ERR: 3,
+            WRONG_DOCUMENT_ERR: 4,
+            NO_MODIFICATION_ALLOWED_ERR: 7,
+            NOT_FOUND_ERR: 8,
+            NOT_SUPPORTED_ERR: 9,
+            INVALID_STATE_ERR: 11,
+            INVALID_NODE_TYPE_ERR: 24
+        };
+
+        DOMException.prototype.toString = function() {
+            return this.message;
+        };
+
+        api.dom = {
+            arrayContains: arrayContains,
+            isHtmlNamespace: isHtmlNamespace,
+            parentElement: parentElement,
+            getNodeIndex: getNodeIndex,
+            getNodeLength: getNodeLength,
+            getCommonAncestor: getCommonAncestor,
+            isAncestorOf: isAncestorOf,
+            isOrIsAncestorOf: isOrIsAncestorOf,
+            getClosestAncestorIn: getClosestAncestorIn,
+            isCharacterDataNode: isCharacterDataNode,
+            isTextOrCommentNode: isTextOrCommentNode,
+            insertAfter: insertAfter,
+            splitDataNode: splitDataNode,
+            getDocument: getDocument,
+            getWindow: getWindow,
+            getIframeWindow: getIframeWindow,
+            getIframeDocument: getIframeDocument,
+            getBody: getBody,
+            isWindow: isWindow,
+            getContentDocument: getContentDocument,
+            getRootContainer: getRootContainer,
+            comparePoints: comparePoints,
+            isBrokenNode: isBrokenNode,
+            inspectNode: inspectNode,
+            getComputedStyleProperty: getComputedStyleProperty,
+            createTestElement: createTestElement,
+            removeNode: removeNode,
+            fragmentFromNodeChildren: fragmentFromNodeChildren,
+            createIterator: createIterator,
+            DomPosition: DomPosition
+        };
+
+        api.DOMException = DOMException;
+    });
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Pure JavaScript implementation of DOM Range
+    api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
+        var dom = api.dom;
+        var util = api.util;
+        var DomPosition = dom.DomPosition;
+        var DOMException = api.DOMException;
+
+        var isCharacterDataNode = dom.isCharacterDataNode;
+        var getNodeIndex = dom.getNodeIndex;
+        var isOrIsAncestorOf = dom.isOrIsAncestorOf;
+        var getDocument = dom.getDocument;
+        var comparePoints = dom.comparePoints;
+        var splitDataNode = dom.splitDataNode;
+        var getClosestAncestorIn = dom.getClosestAncestorIn;
+        var getNodeLength = dom.getNodeLength;
+        var arrayContains = dom.arrayContains;
+        var getRootContainer = dom.getRootContainer;
+        var crashyTextNodes = api.features.crashyTextNodes;
+
+        var removeNode = dom.removeNode;
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // Utility functions
+
+        function isNonTextPartiallySelected(node, range) {
+            return (node.nodeType != 3) &&
+                   (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
+        }
+
+        function getRangeDocument(range) {
+            return range.document || getDocument(range.startContainer);
+        }
+
+        function getRangeRoot(range) {
+            return getRootContainer(range.startContainer);
+        }
+
+        function getBoundaryBeforeNode(node) {
+            return new DomPosition(node.parentNode, getNodeIndex(node));
+        }
+
+        function getBoundaryAfterNode(node) {
+            return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
+        }
+
+        function insertNodeAtPosition(node, n, o) {
+            var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
+            if (isCharacterDataNode(n)) {
+                if (o == n.length) {
+                    dom.insertAfter(node, n);
+                } else {
+                    n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
+                }
+            } else if (o >= n.childNodes.length) {
+                n.appendChild(node);
+            } else {
+                n.insertBefore(node, n.childNodes[o]);
+            }
+            return firstNodeInserted;
+        }
+
+        function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
+            assertRangeValid(rangeA);
+            assertRangeValid(rangeB);
+
+            if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
+                throw new DOMException("WRONG_DOCUMENT_ERR");
+            }
+
+            var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
+                endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
+
+            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
+        }
+
+        function cloneSubtree(iterator) {
+            var partiallySelected;
+            for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
+                partiallySelected = iterator.isPartiallySelectedSubtree();
+                node = node.cloneNode(!partiallySelected);
+                if (partiallySelected) {
+                    subIterator = iterator.getSubtreeIterator();
+                    node.appendChild(cloneSubtree(subIterator));
+                    subIterator.detach();
+                }
+
+                if (node.nodeType == 10) { // DocumentType
+                    throw new DOMException("HIERARCHY_REQUEST_ERR");
+                }
+                frag.appendChild(node);
+            }
+            return frag;
+        }
+
+        function iterateSubtree(rangeIterator, func, iteratorState) {
+            var it, n;
+            iteratorState = iteratorState || { stop: false };
+            for (var node, subRangeIterator; node = rangeIterator.next(); ) {
+                if (rangeIterator.isPartiallySelectedSubtree()) {
+                    if (func(node) === false) {
+                        iteratorState.stop = true;
+                        return;
+                    } else {
+                        // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
+                        // the node selected by the Range.
+                        subRangeIterator = rangeIterator.getSubtreeIterator();
+                        iterateSubtree(subRangeIterator, func, iteratorState);
+                        subRangeIterator.detach();
+                        if (iteratorState.stop) {
+                            return;
+                        }
+                    }
+                } else {
+                    // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
+                    // descendants
+                    it = dom.createIterator(node);
+                    while ( (n = it.next()) ) {
+                        if (func(n) === false) {
+                            iteratorState.stop = true;
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+
+        function deleteSubtree(iterator) {
+            var subIterator;
+            while (iterator.next()) {
+                if (iterator.isPartiallySelectedSubtree()) {
+                    subIterator = iterator.getSubtreeIterator();
+                    deleteSubtree(subIterator);
+                    subIterator.detach();
+                } else {
+                    iterator.remove();
+                }
+            }
+        }
+
+        function extractSubtree(iterator) {
+            for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
+
+                if (iterator.isPartiallySelectedSubtree()) {
+                    node = node.cloneNode(false);
+                    subIterator = iterator.getSubtreeIterator();
+                    node.appendChild(extractSubtree(subIterator));
+                    subIterator.detach();
+                } else {
+                    iterator.remove();
+                }
+                if (node.nodeType == 10) { // DocumentType
+                    throw new DOMException("HIERARCHY_REQUEST_ERR");
+                }
+                frag.appendChild(node);
+            }
+            return frag;
+        }
+
+        function getNodesInRange(range, nodeTypes, filter) {
+            var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
+            var filterExists = !!filter;
+            if (filterNodeTypes) {
+                regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
+            }
+
+            var nodes = [];
+            iterateSubtree(new RangeIterator(range, false), function(node) {
+                if (filterNodeTypes && !regex.test(node.nodeType)) {
+                    return;
+                }
+                if (filterExists && !filter(node)) {
+                    return;
+                }
+                // Don't include a boundary container if it is a character data node and the range does not contain any
+                // of its character data. See issue 190.
+                var sc = range.startContainer;
+                if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
+                    return;
+                }
+
+                var ec = range.endContainer;
+                if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
+                    return;
+                }
+
+                nodes.push(node);
+            });
+            return nodes;
+        }
+
+        function inspect(range) {
+            var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
+            return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
+                    dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
+        }
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
+
+        function RangeIterator(range, clonePartiallySelectedTextNodes) {
+            this.range = range;
+            this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
+
+
+            if (!range.collapsed) {
+                this.sc = range.startContainer;
+                this.so = range.startOffset;
+                this.ec = range.endContainer;
+                this.eo = range.endOffset;
+                var root = range.commonAncestorContainer;
+
+                if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
+                    this.isSingleCharacterDataNode = true;
+                    this._first = this._last = this._next = this.sc;
+                } else {
+                    this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
+                        this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
+                    this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
+                        this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
+                }
+            }
+        }
+
+        RangeIterator.prototype = {
+            _current: null,
+            _next: null,
+            _first: null,
+            _last: null,
+            isSingleCharacterDataNode: false,
+
+            reset: function() {
+                this._current = null;
+                this._next = this._first;
+            },
+
+            hasNext: function() {
+                return !!this._next;
+            },
+
+            next: function() {
+                // Move to next node
+                var current = this._current = this._next;
+                if (current) {
+                    this._next = (current !== this._last) ? current.nextSibling : null;
+
+                    // Check for partially selected text nodes
+                    if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
+                        if (current === this.ec) {
+                            (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
+                        }
+                        if (this._current === this.sc) {
+                            (current = current.cloneNode(true)).deleteData(0, this.so);
+                        }
+                    }
+                }
+
+                return current;
+            },
+
+            remove: function() {
+                var current = this._current, start, end;
+
+                if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
+                    start = (current === this.sc) ? this.so : 0;
+                    end = (current === this.ec) ? this.eo : current.length;
+                    if (start != end) {
+                        current.deleteData(start, end - start);
+                    }
+                } else {
+                    if (current.parentNode) {
+                        removeNode(current);
+                    } else {
+                    }
+                }
+            },
+
+            // Checks if the current node is partially selected
+            isPartiallySelectedSubtree: function() {
+                var current = this._current;
+                return isNonTextPartiallySelected(current, this.range);
+            },
+
+            getSubtreeIterator: function() {
+                var subRange;
+                if (this.isSingleCharacterDataNode) {
+                    subRange = this.range.cloneRange();
+                    subRange.collapse(false);
+                } else {
+                    subRange = new Range(getRangeDocument(this.range));
+                    var current = this._current;
+                    var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
+
+                    if (isOrIsAncestorOf(current, this.sc)) {
+                        startContainer = this.sc;
+                        startOffset = this.so;
+                    }
+                    if (isOrIsAncestorOf(current, this.ec)) {
+                        endContainer = this.ec;
+                        endOffset = this.eo;
+                    }
+
+                    updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
+                }
+                return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
+            },
+
+            detach: function() {
+                this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
+            }
+        };
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
+        var rootContainerNodeTypes = [2, 9, 11];
+        var readonlyNodeTypes = [5, 6, 10, 12];
+        var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
+        var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
+
+        function createAncestorFinder(nodeTypes) {
+            return function(node, selfIsAncestor) {
+                var t, n = selfIsAncestor ? node : node.parentNode;
+                while (n) {
+                    t = n.nodeType;
+                    if (arrayContains(nodeTypes, t)) {
+                        return n;
+                    }
+                    n = n.parentNode;
+                }
+                return null;
+            };
+        }
+
+        var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
+        var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
+        var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
+
+        function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
+            if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
+                throw new DOMException("INVALID_NODE_TYPE_ERR");
+            }
+        }
+
+        function assertValidNodeType(node, invalidTypes) {
+            if (!arrayContains(invalidTypes, node.nodeType)) {
+                throw new DOMException("INVALID_NODE_TYPE_ERR");
+            }
+        }
+
+        function assertValidOffset(node, offset) {
+            if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
+                throw new DOMException("INDEX_SIZE_ERR");
+            }
+        }
+
+        function assertSameDocumentOrFragment(node1, node2) {
+            if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
+                throw new DOMException("WRONG_DOCUMENT_ERR");
+            }
+        }
+
+        function assertNodeNotReadOnly(node) {
+            if (getReadonlyAncestor(node, true)) {
+                throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
+            }
+        }
+
+        function assertNode(node, codeName) {
+            if (!node) {
+                throw new DOMException(codeName);
+            }
+        }
+
+        function isValidOffset(node, offset) {
+            return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
+        }
+
+        function isRangeValid(range) {
+            return (!!range.startContainer && !!range.endContainer &&
+                    !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
+                    getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
+                    isValidOffset(range.startContainer, range.startOffset) &&
+                    isValidOffset(range.endContainer, range.endOffset));
+        }
+
+        function assertRangeValid(range) {
+            if (!isRangeValid(range)) {
+                throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
+            }
+        }
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // Test the browser's innerHTML support to decide how to implement createContextualFragment
+        var styleEl = document.createElement("style");
+        var htmlParsingConforms = false;
+        try {
+            styleEl.innerHTML = "<b>x</b>";
+            htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
+        } catch (e) {
+            // IE 6 and 7 throw
+        }
+
+        api.features.htmlParsingConforms = htmlParsingConforms;
+
+        var createContextualFragment = htmlParsingConforms ?
+
+            // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
+            // discussion and base code for this implementation at issue 67.
+            // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
+            // Thanks to Aleks Williams.
+            function(fragmentStr) {
+                // "Let node the context object's start's node."
+                var node = this.startContainer;
+                var doc = getDocument(node);
+
+                // "If the context object's start's node is null, raise an INVALID_STATE_ERR
+                // exception and abort these steps."
+                if (!node) {
+                    throw new DOMException("INVALID_STATE_ERR");
+                }
+
+                // "Let element be as follows, depending on node's interface:"
+                // Document, Document Fragment: null
+                var el = null;
+
+                // "Element: node"
+                if (node.nodeType == 1) {
+                    el = node;
+
+                // "Text, Comment: node's parentElement"
+                } else if (isCharacterDataNode(node)) {
+                    el = dom.parentElement(node);
+                }
+
+                // "If either element is null or element's ownerDocument is an HTML document
+                // and element's local name is "html" and element's namespace is the HTML
+                // namespace"
+                if (el === null || (
+                    el.nodeName == "HTML" &&
+                    dom.isHtmlNamespace(getDocument(el).documentElement) &&
+                    dom.isHtmlNamespace(el)
+                )) {
+
+                // "let element be a new Element with "body" as its local name and the HTML
+                // namespace as its namespace.""
+                    el = doc.createElement("body");
+                } else {
+                    el = el.cloneNode(false);
+                }
+
+                // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
+                // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
+                // "In either case, the algorithm must be invoked with fragment as the input
+                // and element as the context element."
+                el.innerHTML = fragmentStr;
+
+                // "If this raises an exception, then abort these steps. Otherwise, let new
+                // children be the nodes returned."
+
+                // "Let fragment be a new DocumentFragment."
+                // "Append all new children to fragment."
+                // "Return fragment."
+                return dom.fragmentFromNodeChildren(el);
+            } :
+
+            // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
+            // previous versions of Rangy used (with the exception of using a body element rather than a div)
+            function(fragmentStr) {
+                var doc = getRangeDocument(this);
+                var el = doc.createElement("body");
+                el.innerHTML = fragmentStr;
+
+                return dom.fragmentFromNodeChildren(el);
+            };
+
+        function splitRangeBoundaries(range, positionsToPreserve) {
+            assertRangeValid(range);
+
+            var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
+            var startEndSame = (sc === ec);
+
+            if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
+                splitDataNode(ec, eo, positionsToPreserve);
+            }
+
+            if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
+                sc = splitDataNode(sc, so, positionsToPreserve);
+                if (startEndSame) {
+                    eo -= so;
+                    ec = sc;
+                } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
+                    eo++;
+                }
+                so = 0;
+            }
+            range.setStartAndEnd(sc, so, ec, eo);
+        }
+
+        function rangeToHtml(range) {
+            assertRangeValid(range);
+            var container = range.commonAncestorContainer.parentNode.cloneNode(false);
+            container.appendChild( range.cloneContents() );
+            return container.innerHTML;
+        }
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
+            "commonAncestorContainer"];
+
+        var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
+        var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
+
+        util.extend(api.rangePrototype, {
+            compareBoundaryPoints: function(how, range) {
+                assertRangeValid(this);
+                assertSameDocumentOrFragment(this.startContainer, range.startContainer);
+
+                var nodeA, offsetA, nodeB, offsetB;
+                var prefixA = (how == e2s || how == s2s) ? "start" : "end";
+                var prefixB = (how == s2e || how == s2s) ? "start" : "end";
+                nodeA = this[prefixA + "Container"];
+                offsetA = this[prefixA + "Offset"];
+                nodeB = range[prefixB + "Container"];
+                offsetB = range[prefixB + "Offset"];
+                return comparePoints(nodeA, offsetA, nodeB, offsetB);
+            },
+
+            insertNode: function(node) {
+                assertRangeValid(this);
+                assertValidNodeType(node, insertableNodeTypes);
+                assertNodeNotReadOnly(this.startContainer);
+
+                if (isOrIsAncestorOf(node, this.startContainer)) {
+                    throw new DOMException("HIERARCHY_REQUEST_ERR");
+                }
+
+                // No check for whether the container of the start of the Range is of a type that does not allow
+                // children of the type of node: the browser's DOM implementation should do this for us when we attempt
+                // to add the node
+
+                var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
+                this.setStartBefore(firstNodeInserted);
+            },
+
+            cloneContents: function() {
+                assertRangeValid(this);
+
+                var clone, frag;
+                if (this.collapsed) {
+                    return getRangeDocument(this).createDocumentFragment();
+                } else {
+                    if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
+                        clone = this.startContainer.cloneNode(true);
+                        clone.data = clone.data.slice(this.startOffset, this.endOffset);
+                        frag = getRangeDocument(this).createDocumentFragment();
+                        frag.appendChild(clone);
+                        return frag;
+                    } else {
+                        var iterator = new RangeIterator(this, true);
+                        clone = cloneSubtree(iterator);
+                        iterator.detach();
+                    }
+                    return clone;
+                }
+            },
+
+            canSurroundContents: function() {
+                assertRangeValid(this);
+                assertNodeNotReadOnly(this.startContainer);
+                assertNodeNotReadOnly(this.endContainer);
+
+                // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
+                // no non-text nodes.
+                var iterator = new RangeIterator(this, true);
+                var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
+                        (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
+                iterator.detach();
+                return !boundariesInvalid;
+            },
+
+            surroundContents: function(node) {
+                assertValidNodeType(node, surroundNodeTypes);
+
+                if (!this.canSurroundContents()) {
+                    throw new DOMException("INVALID_STATE_ERR");
+                }
+
+                // Extract the contents
+                var content = this.extractContents();
+
+                // Clear the children of the node
+                if (node.hasChildNodes()) {
+                    while (node.lastChild) {
+                        node.removeChild(node.lastChild);
+                    }
+                }
+
+                // Insert the new node and add the extracted contents
+                insertNodeAtPosition(node, this.startContainer, this.startOffset);
+                node.appendChild(content);
+
+                this.selectNode(node);
+            },
+
+            cloneRange: function() {
+                assertRangeValid(this);
+                var range = new Range(getRangeDocument(this));
+                var i = rangeProperties.length, prop;
+                while (i--) {
+                    prop = rangeProperties[i];
+                    range[prop] = this[prop];
+                }
+                return range;
+            },
+
+            toString: function() {
+                assertRangeValid(this);
+                var sc = this.startContainer;
+                if (sc === this.endContainer && isCharacterDataNode(sc)) {
+                    return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
+                } else {
+                    var textParts = [], iterator = new RangeIterator(this, true);
+                    iterateSubtree(iterator, function(node) {
+                        // Accept only text or CDATA nodes, not comments
+                        if (node.nodeType == 3 || node.nodeType == 4) {
+                            textParts.push(node.data);
+                        }
+                    });
+                    iterator.detach();
+                    return textParts.join("");
+                }
+            },
+
+            // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
+            // been removed from Mozilla.
+
+            compareNode: function(node) {
+                assertRangeValid(this);
+
+                var parent = node.parentNode;
+                var nodeIndex = getNodeIndex(node);
+
+                if (!parent) {
+                    throw new DOMException("NOT_FOUND_ERR");
+                }
+
+                var startComparison = this.comparePoint(parent, nodeIndex),
+                    endComparison = this.comparePoint(parent, nodeIndex + 1);
+
+                if (startComparison < 0) { // Node starts before
+                    return (endComparison > 0) ? n_b_a : n_b;
+                } else {
+                    return (endComparison > 0) ? n_a : n_i;
+                }
+            },
+
+            comparePoint: function(node, offset) {
+                assertRangeValid(this);
+                assertNode(node, "HIERARCHY_REQUEST_ERR");
+                assertSameDocumentOrFragment(node, this.startContainer);
+
+                if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
+                    return -1;
+                } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
+                    return 1;
+                }
+                return 0;
+            },
+
+            createContextualFragment: createContextualFragment,
+
+            toHtml: function() {
+                return rangeToHtml(this);
+            },
+
+            // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
+            // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
+            intersectsNode: function(node, touchingIsIntersecting) {
+                assertRangeValid(this);
+                if (getRootContainer(node) != getRangeRoot(this)) {
+                    return false;
+                }
+
+                var parent = node.parentNode, offset = getNodeIndex(node);
+                if (!parent) {
+                    return true;
+                }
+
+                var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
+                    endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
+
+                return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
+            },
+
+            isPointInRange: function(node, offset) {
+                assertRangeValid(this);
+                assertNode(node, "HIERARCHY_REQUEST_ERR");
+                assertSameDocumentOrFragment(node, this.startContainer);
+
+                return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
+                       (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
+            },
+
+            // The methods below are non-standard and invented by me.
+
+            // Sharing a boundary start-to-end or end-to-start does not count as intersection.
+            intersectsRange: function(range) {
+                return rangesIntersect(this, range, false);
+            },
+
+            // Sharing a boundary start-to-end or end-to-start does count as intersection.
+            intersectsOrTouchesRange: function(range) {
+                return rangesIntersect(this, range, true);
+            },
+
+            intersection: function(range) {
+                if (this.intersectsRange(range)) {
+                    var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
+                        endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
+
+                    var intersectionRange = this.cloneRange();
+                    if (startComparison == -1) {
+                        intersectionRange.setStart(range.startContainer, range.startOffset);
+                    }
+                    if (endComparison == 1) {
+                        intersectionRange.setEnd(range.endContainer, range.endOffset);
+                    }
+                    return intersectionRange;
+                }
+                return null;
+            },
+
+            union: function(range) {
+                if (this.intersectsOrTouchesRange(range)) {
+                    var unionRange = this.cloneRange();
+                    if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
+                        unionRange.setStart(range.startContainer, range.startOffset);
+                    }
+                    if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
+                        unionRange.setEnd(range.endContainer, range.endOffset);
+                    }
+                    return unionRange;
+                } else {
+                    throw new DOMException("Ranges do not intersect");
+                }
+            },
+
+            containsNode: function(node, allowPartial) {
+                if (allowPartial) {
+                    return this.intersectsNode(node, false);
+                } else {
+                    return this.compareNode(node) == n_i;
+                }
+            },
+
+            containsNodeContents: function(node) {
+                return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
+            },
+
+            containsRange: function(range) {
+                var intersection = this.intersection(range);
+                return intersection !== null && range.equals(intersection);
+            },
+
+            containsNodeText: function(node) {
+                var nodeRange = this.cloneRange();
+                nodeRange.selectNode(node);
+                var textNodes = nodeRange.getNodes([3]);
+                if (textNodes.length > 0) {
+                    nodeRange.setStart(textNodes[0], 0);
+                    var lastTextNode = textNodes.pop();
+                    nodeRange.setEnd(lastTextNode, lastTextNode.length);
+                    return this.containsRange(nodeRange);
+                } else {
+                    return this.containsNodeContents(node);
+                }
+            },
+
+            getNodes: function(nodeTypes, filter) {
+                assertRangeValid(this);
+                return getNodesInRange(this, nodeTypes, filter);
+            },
+
+            getDocument: function() {
+                return getRangeDocument(this);
+            },
+
+            collapseBefore: function(node) {
+                this.setEndBefore(node);
+                this.collapse(false);
+            },
+
+            collapseAfter: function(node) {
+                this.setStartAfter(node);
+                this.collapse(true);
+            },
+
+            getBookmark: function(containerNode) {
+                var doc = getRangeDocument(this);
+                var preSelectionRange = api.createRange(doc);
+                containerNode = containerNode || dom.getBody(doc);
+                preSelectionRange.selectNodeContents(containerNode);
+                var range = this.intersection(preSelectionRange);
+                var start = 0, end = 0;
+                if (range) {
+                    preSelectionRange.setEnd(range.startContainer, range.startOffset);
+                    start = preSelectionRange.toString().length;
+                    end = start + range.toString().length;
+                }
+
+                return {
+                    start: start,
+                    end: end,
+                    containerNode: containerNode
+                };
+            },
+
+            moveToBookmark: function(bookmark) {
+                var containerNode = bookmark.containerNode;
+                var charIndex = 0;
+                this.setStart(containerNode, 0);
+                this.collapse(true);
+                var nodeStack = [containerNode], node, foundStart = false, stop = false;
+                var nextCharIndex, i, childNodes;
+
+                while (!stop && (node = nodeStack.pop())) {
+                    if (node.nodeType == 3) {
+                        nextCharIndex = charIndex + node.length;
+                        if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
+                            this.setStart(node, bookmark.start - charIndex);
+                            foundStart = true;
+                        }
+                        if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
+                            this.setEnd(node, bookmark.end - charIndex);
+                            stop = true;
+                        }
+                        charIndex = nextCharIndex;
+                    } else {
+                        childNodes = node.childNodes;
+                        i = childNodes.length;
+                        while (i--) {
+                            nodeStack.push(childNodes[i]);
+                        }
+                    }
+                }
+            },
+
+            getName: function() {
+                return "DomRange";
+            },
+
+            equals: function(range) {
+                return Range.rangesEqual(this, range);
+            },
+
+            isValid: function() {
+                return isRangeValid(this);
+            },
+
+            inspect: function() {
+                return inspect(this);
+            },
+
+            detach: function() {
+                // In DOM4, detach() is now a no-op.
+            }
+        });
+
+        function copyComparisonConstantsToObject(obj) {
+            obj.START_TO_START = s2s;
+            obj.START_TO_END = s2e;
+            obj.END_TO_END = e2e;
+            obj.END_TO_START = e2s;
+
+            obj.NODE_BEFORE = n_b;
+            obj.NODE_AFTER = n_a;
+            obj.NODE_BEFORE_AND_AFTER = n_b_a;
+            obj.NODE_INSIDE = n_i;
+        }
+
+        function copyComparisonConstants(constructor) {
+            copyComparisonConstantsToObject(constructor);
+            copyComparisonConstantsToObject(constructor.prototype);
+        }
+
+        function createRangeContentRemover(remover, boundaryUpdater) {
+            return function() {
+                assertRangeValid(this);
+
+                var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
+
+                var iterator = new RangeIterator(this, true);
+
+                // Work out where to position the range after content removal
+                var node, boundary;
+                if (sc !== root) {
+                    node = getClosestAncestorIn(sc, root, true);
+                    boundary = getBoundaryAfterNode(node);
+                    sc = boundary.node;
+                    so = boundary.offset;
+                }
+
+                // Check none of the range is read-only
+                iterateSubtree(iterator, assertNodeNotReadOnly);
+
+                iterator.reset();
+
+                // Remove the content
+                var returnValue = remover(iterator);
+                iterator.detach();
+
+                // Move to the new position
+                boundaryUpdater(this, sc, so, sc, so);
+
+                return returnValue;
+            };
+        }
+
+        function createPrototypeRange(constructor, boundaryUpdater) {
+            function createBeforeAfterNodeSetter(isBefore, isStart) {
+                return function(node) {
+                    assertValidNodeType(node, beforeAfterNodeTypes);
+                    assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
+
+                    var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
+                    (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
+                };
+            }
+
+            function setRangeStart(range, node, offset) {
+                var ec = range.endContainer, eo = range.endOffset;
+                if (node !== range.startContainer || offset !== range.startOffset) {
+                    // Check the root containers of the range and the new boundary, and also check whether the new boundary
+                    // is after the current end. In either case, collapse the range to the new position
+                    if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
+                        ec = node;
+                        eo = offset;
+                    }
+                    boundaryUpdater(range, node, offset, ec, eo);
+                }
+            }
+
+            function setRangeEnd(range, node, offset) {
+                var sc = range.startContainer, so = range.startOffset;
+                if (node !== range.endContainer || offset !== range.endOffset) {
+                    // Check the root containers of the range and the new boundary, and also check whether the new boundary
+                    // is after the current end. In either case, collapse the range to the new position
+                    if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
+                        sc = node;
+                        so = offset;
+                    }
+                    boundaryUpdater(range, sc, so, node, offset);
+                }
+            }
+
+            // Set up inheritance
+            var F = function() {};
+            F.prototype = api.rangePrototype;
+            constructor.prototype = new F();
+
+            util.extend(constructor.prototype, {
+                setStart: function(node, offset) {
+                    assertNoDocTypeNotationEntityAncestor(node, true);
+                    assertValidOffset(node, offset);
+
+                    setRangeStart(this, node, offset);
+                },
+
+                setEnd: function(node, offset) {
+                    assertNoDocTypeNotationEntityAncestor(node, true);
+                    assertValidOffset(node, offset);
+
+                    setRangeEnd(this, node, offset);
+                },
+
+                /**
+                 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
+                 * - Two parameters (node, offset) creates a collapsed range at that position
+                 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
+                 *   startOffset and ending at endOffset
+                 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
+                 *   startNode and ending at endOffset in endNode
+                 */
+                setStartAndEnd: function() {
+                    var args = arguments;
+                    var sc = args[0], so = args[1], ec = sc, eo = so;
+
+                    switch (args.length) {
+                        case 3:
+                            eo = args[2];
+                            break;
+                        case 4:
+                            ec = args[2];
+                            eo = args[3];
+                            break;
+                    }
+
+                    boundaryUpdater(this, sc, so, ec, eo);
+                },
+
+                setBoundary: function(node, offset, isStart) {
+                    this["set" + (isStart ? "Start" : "End")](node, offset);
+                },
+
+                setStartBefore: createBeforeAfterNodeSetter(true, true),
+                setStartAfter: createBeforeAfterNodeSetter(false, true),
+                setEndBefore: createBeforeAfterNodeSetter(true, false),
+                setEndAfter: createBeforeAfterNodeSetter(false, false),
+
+                collapse: function(isStart) {
+                    assertRangeValid(this);
+                    if (isStart) {
+                        boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
+                    } else {
+                        boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
+                    }
+                },
+
+                selectNodeContents: function(node) {
+                    assertNoDocTypeNotationEntityAncestor(node, true);
+
+                    boundaryUpdater(this, node, 0, node, getNodeLength(node));
+                },
+
+                selectNode: function(node) {
+                    assertNoDocTypeNotationEntityAncestor(node, false);
+                    assertValidNodeType(node, beforeAfterNodeTypes);
+
+                    var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
+                    boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
+                },
+
+                extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
+
+                deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
+
+                canSurroundContents: function() {
+                    assertRangeValid(this);
+                    assertNodeNotReadOnly(this.startContainer);
+                    assertNodeNotReadOnly(this.endContainer);
+
+                    // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
+                    // no non-text nodes.
+                    var iterator = new RangeIterator(this, true);
+                    var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
+                            (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
+                    iterator.detach();
+                    return !boundariesInvalid;
+                },
+
+                splitBoundaries: function() {
+                    splitRangeBoundaries(this);
+                },
+
+                splitBoundariesPreservingPositions: function(positionsToPreserve) {
+                    splitRangeBoundaries(this, positionsToPreserve);
+                },
+
+                normalizeBoundaries: function() {
+                    assertRangeValid(this);
+
+                    var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
+
+                    var mergeForward = function(node) {
+                        var sibling = node.nextSibling;
+                        if (sibling && sibling.nodeType == node.nodeType) {
+                            ec = node;
+                            eo = node.length;
+                            node.appendData(sibling.data);
+                            removeNode(sibling);
+                        }
+                    };
+
+                    var mergeBackward = function(node) {
+                        var sibling = node.previousSibling;
+                        if (sibling && sibling.nodeType == node.nodeType) {
+                            sc = node;
+                            var nodeLength = node.length;
+                            so = sibling.length;
+                            node.insertData(0, sibling.data);
+                            removeNode(sibling);
+                            if (sc == ec) {
+                                eo += so;
+                                ec = sc;
+                            } else if (ec == node.parentNode) {
+                                var nodeIndex = getNodeIndex(node);
+                                if (eo == nodeIndex) {
+                                    ec = node;
+                                    eo = nodeLength;
+                                } else if (eo > nodeIndex) {
+                                    eo--;
+                                }
+                            }
+                        }
+                    };
+
+                    var normalizeStart = true;
+                    var sibling;
+
+                    if (isCharacterDataNode(ec)) {
+                        if (eo == ec.length) {
+                            mergeForward(ec);
+                        } else if (eo == 0) {
+                            sibling = ec.previousSibling;
+                            if (sibling && sibling.nodeType == ec.nodeType) {
+                                eo = sibling.length;
+                                if (sc == ec) {
+                                    normalizeStart = false;
+                                }
+                                sibling.appendData(ec.data);
+                                removeNode(ec);
+                                ec = sibling;
+                            }
+                        }
+                    } else {
+                        if (eo > 0) {
+                            var endNode = ec.childNodes[eo - 1];
+                            if (endNode && isCharacterDataNode(endNode)) {
+                                mergeForward(endNode);
+                            }
+                        }
+                        normalizeStart = !this.collapsed;
+                    }
+
+                    if (normalizeStart) {
+                        if (isCharacterDataNode(sc)) {
+                            if (so == 0) {
+                                mergeBackward(sc);
+                            } else if (so == sc.length) {
+                                sibling = sc.nextSibling;
+                                if (sibling && sibling.nodeType == sc.nodeType) {
+                                    if (ec == sibling) {
+                                        ec = sc;
+                                        eo += sc.length;
+                                    }
+                                    sc.appendData(sibling.data);
+                                    removeNode(sibling);
+                                }
+                            }
+                        } else {
+                            if (so < sc.childNodes.length) {
+                                var startNode = sc.childNodes[so];
+                                if (startNode && isCharacterDataNode(startNode)) {
+                                    mergeBackward(startNode);
+                                }
+                            }
+                        }
+                    } else {
+                        sc = ec;
+                        so = eo;
+                    }
+
+                    boundaryUpdater(this, sc, so, ec, eo);
+                },
+
+                collapseToPoint: function(node, offset) {
+                    assertNoDocTypeNotationEntityAncestor(node, true);
+                    assertValidOffset(node, offset);
+                    this.setStartAndEnd(node, offset);
+                }
+            });
+
+            copyComparisonConstants(constructor);
+        }
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // Updates commonAncestorContainer and collapsed after boundary change
+        function updateCollapsedAndCommonAncestor(range) {
+            range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
+            range.commonAncestorContainer = range.collapsed ?
+                range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
+        }
+
+        function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
+            range.startContainer = startContainer;
+            range.startOffset = startOffset;
+            range.endContainer = endContainer;
+            range.endOffset = endOffset;
+            range.document = dom.getDocument(startContainer);
+
+            updateCollapsedAndCommonAncestor(range);
+        }
+
+        function Range(doc) {
+            this.startContainer = doc;
+            this.startOffset = 0;
+            this.endContainer = doc;
+            this.endOffset = 0;
+            this.document = doc;
+            updateCollapsedAndCommonAncestor(this);
+        }
+
+        createPrototypeRange(Range, updateBoundaries);
+
+        util.extend(Range, {
+            rangeProperties: rangeProperties,
+            RangeIterator: RangeIterator,
+            copyComparisonConstants: copyComparisonConstants,
+            createPrototypeRange: createPrototypeRange,
+            inspect: inspect,
+            toHtml: rangeToHtml,
+            getRangeDocument: getRangeDocument,
+            rangesEqual: function(r1, r2) {
+                return r1.startContainer === r2.startContainer &&
+                    r1.startOffset === r2.startOffset &&
+                    r1.endContainer === r2.endContainer &&
+                    r1.endOffset === r2.endOffset;
+            }
+        });
+
+        api.DomRange = Range;
+    });
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Wrappers for the browser's native DOM Range and/or TextRange implementation
+    api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
+        var WrappedRange, WrappedTextRange;
+        var dom = api.dom;
+        var util = api.util;
+        var DomPosition = dom.DomPosition;
+        var DomRange = api.DomRange;
+        var getBody = dom.getBody;
+        var getContentDocument = dom.getContentDocument;
+        var isCharacterDataNode = dom.isCharacterDataNode;
+
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        if (api.features.implementsDomRange) {
+            // This is a wrapper around the browser's native DOM Range. It has two aims:
+            // - Provide workarounds for specific browser bugs
+            // - provide convenient extensions, which are inherited from Rangy's DomRange
+
+            (function() {
+                var rangeProto;
+                var rangeProperties = DomRange.rangeProperties;
+
+                function updateRangeProperties(range) {
+                    var i = rangeProperties.length, prop;
+                    while (i--) {
+                        prop = rangeProperties[i];
+                        range[prop] = range.nativeRange[prop];
+                    }
+                    // Fix for broken collapsed property in IE 9.
+                    range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
+                }
+
+                function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
+                    var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
+                    var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
+                    var nativeRangeDifferent = !range.equals(range.nativeRange);
+
+                    // Always set both boundaries for the benefit of IE9 (see issue 35)
+                    if (startMoved || endMoved || nativeRangeDifferent) {
+                        range.setEnd(endContainer, endOffset);
+                        range.setStart(startContainer, startOffset);
+                    }
+                }
+
+                var createBeforeAfterNodeSetter;
+
+                WrappedRange = function(range) {
+                    if (!range) {
+                        throw module.createError("WrappedRange: Range must be specified");
+                    }
+                    this.nativeRange = range;
+                    updateRangeProperties(this);
+                };
+
+                DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
+
+                rangeProto = WrappedRange.prototype;
+
+                rangeProto.selectNode = function(node) {
+                    this.nativeRange.selectNode(node);
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.cloneContents = function() {
+                    return this.nativeRange.cloneContents();
+                };
+
+                // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
+                // insertNode() is never delegated to the native range.
+
+                rangeProto.surroundContents = function(node) {
+                    this.nativeRange.surroundContents(node);
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.collapse = function(isStart) {
+                    this.nativeRange.collapse(isStart);
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.cloneRange = function() {
+                    return new WrappedRange(this.nativeRange.cloneRange());
+                };
+
+                rangeProto.refresh = function() {
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.toString = function() {
+                    return this.nativeRange.toString();
+                };
+
+                // Create test range and node for feature detection
+
+                var testTextNode = document.createTextNode("test");
+                getBody(document).appendChild(testTextNode);
+                var range = document.createRange();
+
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
+                // correct for it
+
+                range.setStart(testTextNode, 0);
+                range.setEnd(testTextNode, 0);
+
+                try {
+                    range.setStart(testTextNode, 1);
+
+                    rangeProto.setStart = function(node, offset) {
+                        this.nativeRange.setStart(node, offset);
+                        updateRangeProperties(this);
+                    };
+
+                    rangeProto.setEnd = function(node, offset) {
+                        this.nativeRange.setEnd(node, offset);
+                        updateRangeProperties(this);
+                    };
+
+                    createBeforeAfterNodeSetter = function(name) {
+                        return function(node) {
+                            this.nativeRange[name](node);
+                            updateRangeProperties(this);
+                        };
+                    };
+
+                } catch(ex) {
+
+                    rangeProto.setStart = function(node, offset) {
+                        try {
+                            this.nativeRange.setStart(node, offset);
+                        } catch (ex) {
+                            this.nativeRange.setEnd(node, offset);
+                            this.nativeRange.setStart(node, offset);
+                        }
+                        updateRangeProperties(this);
+                    };
+
+                    rangeProto.setEnd = function(node, offset) {
+                        try {
+                            this.nativeRange.setEnd(node, offset);
+                        } catch (ex) {
+                            this.nativeRange.setStart(node, offset);
+                            this.nativeRange.setEnd(node, offset);
+                        }
+                        updateRangeProperties(this);
+                    };
+
+                    createBeforeAfterNodeSetter = function(name, oppositeName) {
+                        return function(node) {
+                            try {
+                                this.nativeRange[name](node);
+                            } catch (ex) {
+                                this.nativeRange[oppositeName](node);
+                                this.nativeRange[name](node);
+                            }
+                            updateRangeProperties(this);
+                        };
+                    };
+                }
+
+                rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
+                rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
+                rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
+                rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
+
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
+                // whether the native implementation can be trusted
+                rangeProto.selectNodeContents = function(node) {
+                    this.setStartAndEnd(node, 0, dom.getNodeLength(node));
+                };
+
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
+                // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
+
+                range.selectNodeContents(testTextNode);
+                range.setEnd(testTextNode, 3);
+
+                var range2 = document.createRange();
+                range2.selectNodeContents(testTextNode);
+                range2.setEnd(testTextNode, 4);
+                range2.setStart(testTextNode, 2);
+
+                if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
+                        range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
+                    // This is the wrong way round, so correct for it
+
+                    rangeProto.compareBoundaryPoints = function(type, range) {
+                        range = range.nativeRange || range;
+                        if (type == range.START_TO_END) {
+                            type = range.END_TO_START;
+                        } else if (type == range.END_TO_START) {
+                            type = range.START_TO_END;
+                        }
+                        return this.nativeRange.compareBoundaryPoints(type, range);
+                    };
+                } else {
+                    rangeProto.compareBoundaryPoints = function(type, range) {
+                        return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
+                    };
+                }
+
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
+
+                var el = document.createElement("div");
+                el.innerHTML = "123";
+                var textNode = el.firstChild;
+                var body = getBody(document);
+                body.appendChild(el);
+
+                range.setStart(textNode, 1);
+                range.setEnd(textNode, 2);
+                range.deleteContents();
+
+                if (textNode.data == "13") {
+                    // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
+                    // extractContents()
+                    rangeProto.deleteContents = function() {
+                        this.nativeRange.deleteContents();
+                        updateRangeProperties(this);
+                    };
+
+                    rangeProto.extractContents = function() {
+                        var frag = this.nativeRange.extractContents();
+                        updateRangeProperties(this);
+                        return frag;
+                    };
+                } else {
+                }
+
+                body.removeChild(el);
+                body = null;
+
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Test for existence of createContextualFragment and delegate to it if it exists
+                if (util.isHostMethod(range, "createContextualFragment")) {
+                    rangeProto.createContextualFragment = function(fragmentStr) {
+                        return this.nativeRange.createContextualFragment(fragmentStr);
+                    };
+                }
+
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Clean up
+                getBody(document).removeChild(testTextNode);
+
+                rangeProto.getName = function() {
+                    return "WrappedRange";
+                };
+
+                api.WrappedRange = WrappedRange;
+
+                api.createNativeRange = function(doc) {
+                    doc = getContentDocument(doc, module, "createNativeRange");
+                    return doc.createRange();
+                };
+            })();
+        }
+
+        if (api.features.implementsTextRange) {
+            /*
+            This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
+            method. For example, in the following (where pipes denote the selection boundaries):
+
+            <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
+
+            var range = document.selection.createRange();
+            alert(range.parentElement().id); // Should alert "ul" but alerts "b"
+
+            This method returns the common ancestor node of the following:
+            - the parentElement() of the textRange
+            - the parentElement() of the textRange after calling collapse(true)
+            - the parentElement() of the textRange after calling collapse(false)
+            */
+            var getTextRangeContainerElement = function(textRange) {
+                var parentEl = textRange.parentElement();
+                var range = textRange.duplicate();
+                range.collapse(true);
+                var startEl = range.parentElement();
+                range = textRange.duplicate();
+                range.collapse(false);
+                var endEl = range.parentElement();
+                var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
+
+                return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
+            };
+
+            var textRangeIsCollapsed = function(textRange) {
+                return textRange.compareEndPoints("StartToEnd", textRange) == 0;
+            };
+
+            // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
+            // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
+            // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
+            // bugs, handling for inputs and images, plus optimizations.
+            var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
+                var workingRange = textRange.duplicate();
+                workingRange.collapse(isStart);
+                var containerElement = workingRange.parentElement();
+
+                // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
+                // check for that
+                if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
+                    containerElement = wholeRangeContainerElement;
+                }
+
+
+                // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
+                // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
+                if (!containerElement.canHaveHTML) {
+                    var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
+                    return {
+                        boundaryPosition: pos,
+                        nodeInfo: {
+                            nodeIndex: pos.offset,
+                            containerElement: pos.node
+                        }
+                    };
+                }
+
+                var workingNode = dom.getDocument(containerElement).createElement("span");
+
+                // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
+                // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
+                if (workingNode.parentNode) {
+                    dom.removeNode(workingNode);
+                }
+
+                var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
+                var previousNode, nextNode, boundaryPosition, boundaryNode;
+                var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
+                var childNodeCount = containerElement.childNodes.length;
+                var end = childNodeCount;
+
+                // Check end first. Code within the loop assumes that the endth child node of the container is definitely
+                // after the range boundary.
+                var nodeIndex = end;
+
+                while (true) {
+                    if (nodeIndex == childNodeCount) {
+                        containerElement.appendChild(workingNode);
+                    } else {
+                        containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
+                    }
+                    workingRange.moveToElementText(workingNode);
+                    comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
+                    if (comparison == 0 || start == end) {
+                        break;
+                    } else if (comparison == -1) {
+                        if (end == start + 1) {
+                            // We know the endth child node is after the range boundary, so we must be done.
+                            break;
+                        } else {
+                            start = nodeIndex;
+                        }
+                    } else {
+                        end = (end == start + 1) ? start : nodeIndex;
+                    }
+                    nodeIndex = Math.floor((start + end) / 2);
+                    containerElement.removeChild(workingNode);
+                }
+
+
+                // We've now reached or gone past the boundary of the text range we're interested in
+                // so have identified the node we want
+                boundaryNode = workingNode.nextSibling;
+
+                if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
+                    // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
+                    // the node containing the text range's boundary, so we move the end of the working range to the
+                    // boundary point and measure the length of its text to get the boundary's offset within the node.
+                    workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
+
+                    var offset;
+
+                    if (/[\r\n]/.test(boundaryNode.data)) {
+                        /*
+                        For the particular case of a boundary within a text node containing rendered line breaks (within a
+                        <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
+                        IE. The facts:
+
+                        - Each line break is represented as \r in the text node's data/nodeValue properties
+                        - Each line break is represented as \r\n in the TextRange's 'text' property
+                        - The 'text' property of the TextRange does not contain trailing line breaks
+
+                        To get round the problem presented by the final fact above, we can use the fact that TextRange's
+                        moveStart() and moveEnd() methods return the actual number of characters moved, which is not
+                        necessarily the same as the number of characters it was instructed to move. The simplest approach is
+                        to use this to store the characters moved when moving both the start and end of the range to the
+                        start of the document body and subtracting the start offset from the end offset (the
+                        "move-negative-gazillion" method). However, this is extremely slow when the document is large and
+                        the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
+                        the end of the document) has the same problem.
+
+                        Another approach that works is to use moveStart() to move the start boundary of the range up to the
+                        end boundary one character at a time and incrementing a counter with the value returned by the
+                        moveStart() call. However, the check for whether the start boundary has reached the end boundary is
+                        expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
+                        by the location of the range within the document).
+
+                        The approach used below is a hybrid of the two methods above. It uses the fact that a string
+                        containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
+                        be longer than the text of the TextRange, so the start of the range is moved that length initially
+                        and then a character at a time to make up for any trailing line breaks not contained in the 'text'
+                        property. This has good performance in most situations compared to the previous two methods.
+                        */
+                        var tempRange = workingRange.duplicate();
+                        var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
+
+                        offset = tempRange.moveStart("character", rangeLength);
+                        while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
+                            offset++;
+                            tempRange.moveStart("character", 1);
+                        }
+                    } else {
+                        offset = workingRange.text.length;
+                    }
+                    boundaryPosition = new DomPosition(boundaryNode, offset);
+                } else {
+
+                    // If the boundary immediately follows a character data node and this is the end boundary, we should favour
+                    // a position within that, and likewise for a start boundary preceding a character data node
+                    previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
+                    nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
+                    if (nextNode && isCharacterDataNode(nextNode)) {
+                        boundaryPosition = new DomPosition(nextNode, 0);
+                    } else if (previousNode && isCharacterDataNode(previousNode)) {
+                        boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
+                    } else {
+                        boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+                    }
+                }
+
+                // Clean up
+                dom.removeNode(workingNode);
+
+                return {
+                    boundaryPosition: boundaryPosition,
+                    nodeInfo: {
+                        nodeIndex: nodeIndex,
+                        containerElement: containerElement
+                    }
+                };
+            };
+
+            // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
+            // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
+            // (http://code.google.com/p/ierange/)
+            var createBoundaryTextRange = function(boundaryPosition, isStart) {
+                var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+                var doc = dom.getDocument(boundaryPosition.node);
+                var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
+                var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
+
+                if (nodeIsDataNode) {
+                    boundaryNode = boundaryPosition.node;
+                    boundaryParent = boundaryNode.parentNode;
+                } else {
+                    childNodes = boundaryPosition.node.childNodes;
+                    boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
+                    boundaryParent = boundaryPosition.node;
+                }
+
+                // Position the range immediately before the node containing the boundary
+                workingNode = doc.createElement("span");
+
+                // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
+                // the element rather than immediately before or after it
+                workingNode.innerHTML = "&#feff;";
+
+                // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
+                // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
+                if (boundaryNode) {
+                    boundaryParent.insertBefore(workingNode, boundaryNode);
+                } else {
+                    boundaryParent.appendChild(workingNode);
+                }
+
+                workingRange.moveToElementText(workingNode);
+                workingRange.collapse(!isStart);
+
+                // Clean up
+                boundaryParent.removeChild(workingNode);
+
+                // Move the working range to the text offset, if required
+                if (nodeIsDataNode) {
+                    workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
+                }
+
+                return workingRange;
+            };
+
+            /*------------------------------------------------------------------------------------------------------------*/
+
+            // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+            // prototype
+
+            WrappedTextRange = function(textRange) {
+                this.textRange = textRange;
+                this.refresh();
+            };
+
+            WrappedTextRange.prototype = new DomRange(document);
+
+            WrappedTextRange.prototype.refresh = function() {
+                var start, end, startBoundary;
+
+                // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
+                var rangeContainerElement = getTextRangeContainerElement(this.textRange);
+
+                if (textRangeIsCollapsed(this.textRange)) {
+                    end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
+                        true).boundaryPosition;
+                } else {
+                    startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+                    start = startBoundary.boundaryPosition;
+
+                    // An optimization used here is that if the start and end boundaries have the same parent element, the
+                    // search scope for the end boundary can be limited to exclude the portion of the element that precedes
+                    // the start boundary
+                    end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
+                        startBoundary.nodeInfo).boundaryPosition;
+                }
+
+                this.setStart(start.node, start.offset);
+                this.setEnd(end.node, end.offset);
+            };
+
+            WrappedTextRange.prototype.getName = function() {
+                return "WrappedTextRange";
+            };
+
+            DomRange.copyComparisonConstants(WrappedTextRange);
+
+            var rangeToTextRange = function(range) {
+                if (range.collapsed) {
+                    return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                } else {
+                    var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                    var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
+                    var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
+                    textRange.setEndPoint("StartToStart", startRange);
+                    textRange.setEndPoint("EndToEnd", endRange);
+                    return textRange;
+                }
+            };
+
+            WrappedTextRange.rangeToTextRange = rangeToTextRange;
+
+            WrappedTextRange.prototype.toTextRange = function() {
+                return rangeToTextRange(this);
+            };
+
+            api.WrappedTextRange = WrappedTextRange;
+
+            // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
+            // implementation to use by default.
+            if (!api.features.implementsDomRange || api.config.preferTextRange) {
+                // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
+                var globalObj = (function(f) { return f("return this;")(); })(Function);
+                if (typeof globalObj.Range == "undefined") {
+                    globalObj.Range = WrappedTextRange;
+                }
+
+                api.createNativeRange = function(doc) {
+                    doc = getContentDocument(doc, module, "createNativeRange");
+                    return getBody(doc).createTextRange();
+                };
+
+                api.WrappedRange = WrappedTextRange;
+            }
+        }
+
+        api.createRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRange");
+            return new api.WrappedRange(api.createNativeRange(doc));
+        };
+
+        api.createRangyRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRangyRange");
+            return new DomRange(doc);
+        };
+
+        util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
+        util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
+
+        api.addShimListener(function(win) {
+            var doc = win.document;
+            if (typeof doc.createRange == "undefined") {
+                doc.createRange = function() {
+                    return api.createRange(doc);
+                };
+            }
+            doc = win = null;
+        });
+    });
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
+    // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
+    api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
+        api.config.checkSelectionRanges = true;
+
+        var BOOLEAN = "boolean";
+        var NUMBER = "number";
+        var dom = api.dom;
+        var util = api.util;
+        var isHostMethod = util.isHostMethod;
+        var DomRange = api.DomRange;
+        var WrappedRange = api.WrappedRange;
+        var DOMException = api.DOMException;
+        var DomPosition = dom.DomPosition;
+        var getNativeSelection;
+        var selectionIsCollapsed;
+        var features = api.features;
+        var CONTROL = "Control";
+        var getDocument = dom.getDocument;
+        var getBody = dom.getBody;
+        var rangesEqual = DomRange.rangesEqual;
+
+
+        // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
+        // "forward" or "forwards") or a Boolean (true for backwards).
+        function isDirectionBackward(dir) {
+            return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
+        }
+
+        function getWindow(win, methodName) {
+            if (!win) {
+                return window;
+            } else if (dom.isWindow(win)) {
+                return win;
+            } else if (win instanceof WrappedSelection) {
+                return win.win;
+            } else {
+                var doc = dom.getContentDocument(win, module, methodName);
+                return dom.getWindow(doc);
+            }
+        }
+
+        function getWinSelection(winParam) {
+            return getWindow(winParam, "getWinSelection").getSelection();
+        }
+
+        function getDocSelection(winParam) {
+            return getWindow(winParam, "getDocSelection").document.selection;
+        }
+
+        function winSelectionIsBackward(sel) {
+            var backward = false;
+            if (sel.anchorNode) {
+                backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
+            }
+            return backward;
+        }
+
+        // Test for the Range/TextRange and Selection features required
+        // Test for ability to retrieve selection
+        var implementsWinGetSelection = isHostMethod(window, "getSelection"),
+            implementsDocSelection = util.isHostObject(document, "selection");
+
+        features.implementsWinGetSelection = implementsWinGetSelection;
+        features.implementsDocSelection = implementsDocSelection;
+
+        var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
+
+        if (useDocumentSelection) {
+            getNativeSelection = getDocSelection;
+            api.isSelectionValid = function(winParam) {
+                var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
+
+                // Check whether the selection TextRange is actually contained within the correct document
+                return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
+            };
+        } else if (implementsWinGetSelection) {
+            getNativeSelection = getWinSelection;
+            api.isSelectionValid = function() {
+                return true;
+            };
+        } else {
+            module.fail("Neither document.selection or window.getSelection() detected.");
+            return false;
+        }
+
+        api.getNativeSelection = getNativeSelection;
+
+        var testSelection = getNativeSelection();
+
+        // In Firefox, the selection is null in an iframe with display: none. See issue #138.
+        if (!testSelection) {
+            module.fail("Native selection was null (possibly issue 138?)");
+            return false;
+        }
+
+        var testRange = api.createNativeRange(document);
+        var body = getBody(document);
+
+        // Obtaining a range from a selection
+        var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
+            ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
+
+        features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
+
+        // Test for existence of native selection extend() method
+        var selectionHasExtend = isHostMethod(testSelection, "extend");
+        features.selectionHasExtend = selectionHasExtend;
+
+        // Test if rangeCount exists
+        var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
+        features.selectionHasRangeCount = selectionHasRangeCount;
+
+        var selectionSupportsMultipleRanges = false;
+        var collapsedNonEditableSelectionsSupported = true;
+
+        var addRangeBackwardToNative = selectionHasExtend ?
+            function(nativeSelection, range) {
+                var doc = DomRange.getRangeDocument(range);
+                var endRange = api.createRange(doc);
+                endRange.collapseToPoint(range.endContainer, range.endOffset);
+                nativeSelection.addRange(getNativeRange(endRange));
+                nativeSelection.extend(range.startContainer, range.startOffset);
+            } : null;
+
+        if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
+                typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
+
+            (function() {
+                // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
+                // performed on the current document's selection. See issue 109.
+
+                // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
+                // will result in the selection direction begin reversed if the original selection was backwards and the
+                // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
+                var sel = window.getSelection();
+                if (sel) {
+                    // Store the current selection
+                    var originalSelectionRangeCount = sel.rangeCount;
+                    var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
+                    var originalSelectionRanges = [];
+                    var originalSelectionBackward = winSelectionIsBackward(sel);
+                    for (var i = 0; i < originalSelectionRangeCount; ++i) {
+                        originalSelectionRanges[i] = sel.getRangeAt(i);
+                    }
+
+                    // Create some test elements
+                    var testEl = dom.createTestElement(document, "", false);
+                    var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
+
+                    // Test whether the native selection will allow a collapsed selection within a non-editable element
+                    var r1 = document.createRange();
+
+                    r1.setStart(textNode, 1);
+                    r1.collapse(true);
+                    sel.removeAllRanges();
+                    sel.addRange(r1);
+                    collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
+                    sel.removeAllRanges();
+
+                    // Test whether the native selection is capable of supporting multiple ranges.
+                    if (!selectionHasMultipleRanges) {
+                        // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
+                        // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
+                        // nothing we can do about this while retaining the feature test so we have to resort to a browser
+                        // sniff. I'm not happy about it. See
+                        // https://code.google.com/p/chromium/issues/detail?id=399791
+                        var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
+                        if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
+                            selectionSupportsMultipleRanges = false;
+                        } else {
+                            var r2 = r1.cloneRange();
+                            r1.setStart(textNode, 0);
+                            r2.setEnd(textNode, 3);
+                            r2.setStart(textNode, 2);
+                            sel.addRange(r1);
+                            sel.addRange(r2);
+                            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
+                        }
+                    }
+
+                    // Clean up
+                    dom.removeNode(testEl);
+                    sel.removeAllRanges();
+
+                    for (i = 0; i < originalSelectionRangeCount; ++i) {
+                        if (i == 0 && originalSelectionBackward) {
+                            if (addRangeBackwardToNative) {
+                                addRangeBackwardToNative(sel, originalSelectionRanges[i]);
+                            } else {
+                                api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
+                                sel.addRange(originalSelectionRanges[i]);
+                            }
+                        } else {
+                            sel.addRange(originalSelectionRanges[i]);
+                        }
+                    }
+                }
+            })();
+        }
+
+        features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
+        features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+
+        // ControlRanges
+        var implementsControlRange = false, testControlRange;
+
+        if (body && isHostMethod(body, "createControlRange")) {
+            testControlRange = body.createControlRange();
+            if (util.areHostProperties(testControlRange, ["item", "add"])) {
+                implementsControlRange = true;
+            }
+        }
+        features.implementsControlRange = implementsControlRange;
+
+        // Selection collapsedness
+        if (selectionHasAnchorAndFocus) {
+            selectionIsCollapsed = function(sel) {
+                return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
+            };
+        } else {
+            selectionIsCollapsed = function(sel) {
+                return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
+            };
+        }
+
+        function updateAnchorAndFocusFromRange(sel, range, backward) {
+            var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
+            sel.anchorNode = range[anchorPrefix + "Container"];
+            sel.anchorOffset = range[anchorPrefix + "Offset"];
+            sel.focusNode = range[focusPrefix + "Container"];
+            sel.focusOffset = range[focusPrefix + "Offset"];
+        }
+
+        function updateAnchorAndFocusFromNativeSelection(sel) {
+            var nativeSel = sel.nativeSelection;
+            sel.anchorNode = nativeSel.anchorNode;
+            sel.anchorOffset = nativeSel.anchorOffset;
+            sel.focusNode = nativeSel.focusNode;
+            sel.focusOffset = nativeSel.focusOffset;
+        }
+
+        function updateEmptySelection(sel) {
+            sel.anchorNode = sel.focusNode = null;
+            sel.anchorOffset = sel.focusOffset = 0;
+            sel.rangeCount = 0;
+            sel.isCollapsed = true;
+            sel._ranges.length = 0;
+        }
+
+        function getNativeRange(range) {
+            var nativeRange;
+            if (range instanceof DomRange) {
+                nativeRange = api.createNativeRange(range.getDocument());
+                nativeRange.setEnd(range.endContainer, range.endOffset);
+                nativeRange.setStart(range.startContainer, range.startOffset);
+            } else if (range instanceof WrappedRange) {
+                nativeRange = range.nativeRange;
+            } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
+                nativeRange = range;
+            }
+            return nativeRange;
+        }
+
+        function rangeContainsSingleElement(rangeNodes) {
+            if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
+                return false;
+            }
+            for (var i = 1, len = rangeNodes.length; i < len; ++i) {
+                if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        function getSingleElementFromRange(range) {
+            var nodes = range.getNodes();
+            if (!rangeContainsSingleElement(nodes)) {
+                throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
+            }
+            return nodes[0];
+        }
+
+        // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
+        function isTextRange(range) {
+            return !!range && typeof range.text != "undefined";
+        }
+
+        function updateFromTextRange(sel, range) {
+            // Create a Range from the selected TextRange
+            var wrappedRange = new WrappedRange(range);
+            sel._ranges = [wrappedRange];
+
+            updateAnchorAndFocusFromRange(sel, wrappedRange, false);
+            sel.rangeCount = 1;
+            sel.isCollapsed = wrappedRange.collapsed;
+        }
+
+        function updateControlSelection(sel) {
+            // Update the wrapped selection based on what's now in the native selection
+            sel._ranges.length = 0;
+            if (sel.docSelection.type == "None") {
+                updateEmptySelection(sel);
+            } else {
+                var controlRange = sel.docSelection.createRange();
+                if (isTextRange(controlRange)) {
+                    // This case (where the selection type is "Control" and calling createRange() on the selection returns
+                    // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
+                    // ControlRange have been removed from the ControlRange and removed from the document.
+                    updateFromTextRange(sel, controlRange);
+                } else {
+                    sel.rangeCount = controlRange.length;
+                    var range, doc = getDocument(controlRange.item(0));
+                    for (var i = 0; i < sel.rangeCount; ++i) {
+                        range = api.createRange(doc);
+                        range.selectNode(controlRange.item(i));
+                        sel._ranges.push(range);
+                    }
+                    sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
+                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
+                }
+            }
+        }
+
+        function addRangeToControlSelection(sel, range) {
+            var controlRange = sel.docSelection.createRange();
+            var rangeElement = getSingleElementFromRange(range);
+
+            // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
+            // contained by the supplied range
+            var doc = getDocument(controlRange.item(0));
+            var newControlRange = getBody(doc).createControlRange();
+            for (var i = 0, len = controlRange.length; i < len; ++i) {
+                newControlRange.add(controlRange.item(i));
+            }
+            try {
+                newControlRange.add(rangeElement);
+            } catch (ex) {
+                throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
+            }
+            newControlRange.select();
+
+            // Update the wrapped selection based on what's now in the native selection
+            updateControlSelection(sel);
+        }
+
+        var getSelectionRangeAt;
+
+        if (isHostMethod(testSelection, "getRangeAt")) {
+            // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
+            // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
+            // lesson to us all, especially me.
+            getSelectionRangeAt = function(sel, index) {
+                try {
+                    return sel.getRangeAt(index);
+                } catch (ex) {
+                    return null;
+                }
+            };
+        } else if (selectionHasAnchorAndFocus) {
+            getSelectionRangeAt = function(sel) {
+                var doc = getDocument(sel.anchorNode);
+                var range = api.createRange(doc);
+                range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
+
+                // Handle the case when the selection was selected backwards (from the end to the start in the
+                // document)
+                if (range.collapsed !== this.isCollapsed) {
+                    range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
+                }
+
+                return range;
+            };
+        }
+
+        function WrappedSelection(selection, docSelection, win) {
+            this.nativeSelection = selection;
+            this.docSelection = docSelection;
+            this._ranges = [];
+            this.win = win;
+            this.refresh();
+        }
+
+        WrappedSelection.prototype = api.selectionPrototype;
+
+        function deleteProperties(sel) {
+            sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
+            sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
+            sel.detached = true;
+        }
+
+        var cachedRangySelections = [];
+
+        function actOnCachedSelection(win, action) {
+            var i = cachedRangySelections.length, cached, sel;
+            while (i--) {
+                cached = cachedRangySelections[i];
+                sel = cached.selection;
+                if (action == "deleteAll") {
+                    deleteProperties(sel);
+                } else if (cached.win == win) {
+                    if (action == "delete") {
+                        cachedRangySelections.splice(i, 1);
+                        return true;
+                    } else {
+                        return sel;
+                    }
+                }
+            }
+            if (action == "deleteAll") {
+                cachedRangySelections.length = 0;
+            }
+            return null;
+        }
+
+        var getSelection = function(win) {
+            // Check if the parameter is a Rangy Selection object
+            if (win && win instanceof WrappedSelection) {
+                win.refresh();
+                return win;
+            }
+
+            win = getWindow(win, "getNativeSelection");
+
+            var sel = actOnCachedSelection(win);
+            var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
+            if (sel) {
+                sel.nativeSelection = nativeSel;
+                sel.docSelection = docSel;
+                sel.refresh();
+            } else {
+                sel = new WrappedSelection(nativeSel, docSel, win);
+                cachedRangySelections.push( { win: win, selection: sel } );
+            }
+            return sel;
+        };
+
+        api.getSelection = getSelection;
+
+        util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
+
+        var selProto = WrappedSelection.prototype;
+
+        function createControlSelection(sel, ranges) {
+            // Ensure that the selection becomes of type "Control"
+            var doc = getDocument(ranges[0].startContainer);
+            var controlRange = getBody(doc).createControlRange();
+            for (var i = 0, el, len = ranges.length; i < len; ++i) {
+                el = getSingleElementFromRange(ranges[i]);
+                try {
+                    controlRange.add(el);
+                } catch (ex) {
+                    throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
+                }
+            }
+            controlRange.select();
+
+            // Update the wrapped selection based on what's now in the native selection
+            updateControlSelection(sel);
+        }
+
+        // Selecting a range
+        if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
+            selProto.removeAllRanges = function() {
+                this.nativeSelection.removeAllRanges();
+                updateEmptySelection(this);
+            };
+
+            var addRangeBackward = function(sel, range) {
+                addRangeBackwardToNative(sel.nativeSelection, range);
+                sel.refresh();
+            };
+
+            if (selectionHasRangeCount) {
+                selProto.addRange = function(range, direction) {
+                    if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                        addRangeToControlSelection(this, range);
+                    } else {
+                        if (isDirectionBackward(direction) && selectionHasExtend) {
+                            addRangeBackward(this, range);
+                        } else {
+                            var previousRangeCount;
+                            if (selectionSupportsMultipleRanges) {
+                                previousRangeCount = this.rangeCount;
+                            } else {
+                                this.removeAllRanges();
+                                previousRangeCount = 0;
+                            }
+                            // Clone the native range so that changing the selected range does not affect the selection.
+                            // This is contrary to the spec but is the only way to achieve consistency between browsers. See
+                            // issue 80.
+                            var clonedNativeRange = getNativeRange(range).cloneRange();
+                            try {
+                                this.nativeSelection.addRange(clonedNativeRange);
+                            } catch (ex) {
+                            }
+
+                            // Check whether adding the range was successful
+                            this.rangeCount = this.nativeSelection.rangeCount;
+
+                            if (this.rangeCount == previousRangeCount + 1) {
+                                // The range was added successfully
+
+                                // Check whether the range that we added to the selection is reflected in the last range extracted from
+                                // the selection
+                                if (api.config.checkSelectionRanges) {
+                                    var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
+                                    if (nativeRange && !rangesEqual(nativeRange, range)) {
+                                        // Happens in WebKit with, for example, a selection placed at the start of a text node
+                                        range = new WrappedRange(nativeRange);
+                                    }
+                                }
+                                this._ranges[this.rangeCount - 1] = range;
+                                updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
+                                this.isCollapsed = selectionIsCollapsed(this);
+                            } else {
+                                // The range was not added successfully. The simplest thing is to refresh
+                                this.refresh();
+                            }
+                        }
+                    }
+                };
+            } else {
+                selProto.addRange = function(range, direction) {
+                    if (isDirectionBackward(direction) && selectionHasExtend) {
+                        addRangeBackward(this, range);
+                    } else {
+                        this.nativeSelection.addRange(getNativeRange(range));
+                        this.refresh();
+                    }
+                };
+            }
+
+            selProto.setRanges = function(ranges) {
+                if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
+                    createControlSelection(this, ranges);
+                } else {
+                    this.removeAllRanges();
+                    for (var i = 0, len = ranges.length; i < len; ++i) {
+                        this.addRange(ranges[i]);
+                    }
+                }
+            };
+        } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
+                   implementsControlRange && useDocumentSelection) {
+
+            selProto.removeAllRanges = function() {
+                // Added try/catch as fix for issue #21
+                try {
+                    this.docSelection.empty();
+
+                    // Check for empty() not working (issue #24)
+                    if (this.docSelection.type != "None") {
+                        // Work around failure to empty a control selection by instead selecting a TextRange and then
+                        // calling empty()
+                        var doc;
+                        if (this.anchorNode) {
+                            doc = getDocument(this.anchorNode);
+                        } else if (this.docSelection.type == CONTROL) {
+                            var controlRange = this.docSelection.createRange();
+                            if (controlRange.length) {
+                                doc = getDocument( controlRange.item(0) );
+                            }
+                        }
+                        if (doc) {
+                            var textRange = getBody(doc).createTextRange();
+                            textRange.select();
+                            this.docSelection.empty();
+                        }
+                    }
+                } catch(ex) {}
+                updateEmptySelection(this);
+            };
+
+            selProto.addRange = function(range) {
+                if (this.docSelection.type == CONTROL) {
+                    addRangeToControlSelection(this, range);
+                } else {
+                    api.WrappedTextRange.rangeToTextRange(range).select();
+                    this._ranges[0] = range;
+                    this.rangeCount = 1;
+                    this.isCollapsed = this._ranges[0].collapsed;
+                    updateAnchorAndFocusFromRange(this, range, false);
+                }
+            };
+
+            selProto.setRanges = function(ranges) {
+                this.removeAllRanges();
+                var rangeCount = ranges.length;
+                if (rangeCount > 1) {
+                    createControlSelection(this, ranges);
+                } else if (rangeCount) {
+                    this.addRange(ranges[0]);
+                }
+            };
+        } else {
+            module.fail("No means of selecting a Range or TextRange was found");
+            return false;
+        }
+
+        selProto.getRangeAt = function(index) {
+            if (index < 0 || index >= this.rangeCount) {
+                throw new DOMException("INDEX_SIZE_ERR");
+            } else {
+                // Clone the range to preserve selection-range independence. See issue 80.
+                return this._ranges[index].cloneRange();
+            }
+        };
+
+        var refreshSelection;
+
+        if (useDocumentSelection) {
+            refreshSelection = function(sel) {
+                var range;
+                if (api.isSelectionValid(sel.win)) {
+                    range = sel.docSelection.createRange();
+                } else {
+                    range = getBody(sel.win.document).createTextRange();
+                    range.collapse(true);
+                }
+
+                if (sel.docSelection.type == CONTROL) {
+                    updateControlSelection(sel);
+                } else if (isTextRange(range)) {
+                    updateFromTextRange(sel, range);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            };
+        } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
+            refreshSelection = function(sel) {
+                if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
+                    updateControlSelection(sel);
+                } else {
+                    sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
+                    if (sel.rangeCount) {
+                        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                            sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
+                        }
+                        updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
+                        sel.isCollapsed = selectionIsCollapsed(sel);
+                    } else {
+                        updateEmptySelection(sel);
+                    }
+                }
+            };
+        } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
+            refreshSelection = function(sel) {
+                var range, nativeSel = sel.nativeSelection;
+                if (nativeSel.anchorNode) {
+                    range = getSelectionRangeAt(nativeSel, 0);
+                    sel._ranges = [range];
+                    sel.rangeCount = 1;
+                    updateAnchorAndFocusFromNativeSelection(sel);
+                    sel.isCollapsed = selectionIsCollapsed(sel);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            };
+        } else {
+            module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
+            return false;
+        }
+
+        selProto.refresh = function(checkForChanges) {
+            var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
+            var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
+
+            refreshSelection(this);
+            if (checkForChanges) {
+                // Check the range count first
+                var i = oldRanges.length;
+                if (i != this._ranges.length) {
+                    return true;
+                }
+
+                // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
+                // ranges after this
+                if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
+                    return true;
+                }
+
+                // Finally, compare each range in turn
+                while (i--) {
+                    if (!rangesEqual(oldRanges[i], this._ranges[i])) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        };
+
+        // Removal of a single range
+        var removeRangeManually = function(sel, range) {
+            var ranges = sel.getAllRanges();
+            sel.removeAllRanges();
+            for (var i = 0, len = ranges.length; i < len; ++i) {
+                if (!rangesEqual(range, ranges[i])) {
+                    sel.addRange(ranges[i]);
+                }
+            }
+            if (!sel.rangeCount) {
+                updateEmptySelection(sel);
+            }
+        };
+
+        if (implementsControlRange && implementsDocSelection) {
+            selProto.removeRange = function(range) {
+                if (this.docSelection.type == CONTROL) {
+                    var controlRange = this.docSelection.createRange();
+                    var rangeElement = getSingleElementFromRange(range);
+
+                    // Create a new ControlRange containing all the elements in the selected ControlRange minus the
+                    // element contained by the supplied range
+                    var doc = getDocument(controlRange.item(0));
+                    var newControlRange = getBody(doc).createControlRange();
+                    var el, removed = false;
+                    for (var i = 0, len = controlRange.length; i < len; ++i) {
+                        el = controlRange.item(i);
+                        if (el !== rangeElement || removed) {
+                            newControlRange.add(controlRange.item(i));
+                        } else {
+                            removed = true;
+                        }
+                    }
+                    newControlRange.select();
+
+                    // Update the wrapped selection based on what's now in the native selection
+                    updateControlSelection(this);
+                } else {
+                    removeRangeManually(this, range);
+                }
+            };
+        } else {
+            selProto.removeRange = function(range) {
+                removeRangeManually(this, range);
+            };
+        }
+
+        // Detecting if a selection is backward
+        var selectionIsBackward;
+        if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
+            selectionIsBackward = winSelectionIsBackward;
+
+            selProto.isBackward = function() {
+                return selectionIsBackward(this);
+            };
+        } else {
+            selectionIsBackward = selProto.isBackward = function() {
+                return false;
+            };
+        }
+
+        // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
+        selProto.isBackwards = selProto.isBackward;
+
+        // Selection stringifier
+        // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
+        // The current spec does not yet define this method.
+        selProto.toString = function() {
+            var rangeTexts = [];
+            for (var i = 0, len = this.rangeCount; i < len; ++i) {
+                rangeTexts[i] = "" + this._ranges[i];
+            }
+            return rangeTexts.join("");
+        };
+
+        function assertNodeInSameDocument(sel, node) {
+            if (sel.win.document != getDocument(node)) {
+                throw new DOMException("WRONG_DOCUMENT_ERR");
+            }
+        }
+
+        // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
+        selProto.collapse = function(node, offset) {
+            assertNodeInSameDocument(this, node);
+            var range = api.createRange(node);
+            range.collapseToPoint(node, offset);
+            this.setSingleRange(range);
+            this.isCollapsed = true;
+        };
+
+        selProto.collapseToStart = function() {
+            if (this.rangeCount) {
+                var range = this._ranges[0];
+                this.collapse(range.startContainer, range.startOffset);
+            } else {
+                throw new DOMException("INVALID_STATE_ERR");
+            }
+        };
+
+        selProto.collapseToEnd = function() {
+            if (this.rangeCount) {
+                var range = this._ranges[this.rangeCount - 1];
+                this.collapse(range.endContainer, range.endOffset);
+            } else {
+                throw new DOMException("INVALID_STATE_ERR");
+            }
+        };
+
+        // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
+        // specified so the native implementation is never used by Rangy.
+        selProto.selectAllChildren = function(node) {
+            assertNodeInSameDocument(this, node);
+            var range = api.createRange(node);
+            range.selectNodeContents(node);
+            this.setSingleRange(range);
+        };
+
+        selProto.deleteFromDocument = function() {
+            // Sepcial behaviour required for IE's control selections
+            if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                var controlRange = this.docSelection.createRange();
+                var element;
+                while (controlRange.length) {
+                    element = controlRange.item(0);
+                    controlRange.remove(element);
+                    dom.removeNode(element);
+                }
+                this.refresh();
+            } else if (this.rangeCount) {
+                var ranges = this.getAllRanges();
+                if (ranges.length) {
+                    this.removeAllRanges();
+                    for (var i = 0, len = ranges.length; i < len; ++i) {
+                        ranges[i].deleteContents();
+                    }
+                    // The spec says nothing about what the selection should contain after calling deleteContents on each
+                    // range. Firefox moves the selection to where the final selected range was, so we emulate that
+                    this.addRange(ranges[len - 1]);
+                }
+            }
+        };
+
+        // The following are non-standard extensions
+        selProto.eachRange = function(func, returnValue) {
+            for (var i = 0, len = this._ranges.length; i < len; ++i) {
+                if ( func( this.getRangeAt(i) ) ) {
+                    return returnValue;
+                }
+            }
+        };
+
+        selProto.getAllRanges = function() {
+            var ranges = [];
+            this.eachRange(function(range) {
+                ranges.push(range);
+            });
+            return ranges;
+        };
+
+        selProto.setSingleRange = function(range, direction) {
+            this.removeAllRanges();
+            this.addRange(range, direction);
+        };
+
+        selProto.callMethodOnEachRange = function(methodName, params) {
+            var results = [];
+            this.eachRange( function(range) {
+                results.push( range[methodName].apply(range, params || []) );
+            } );
+            return results;
+        };
+
+        function createStartOrEndSetter(isStart) {
+            return function(node, offset) {
+                var range;
+                if (this.rangeCount) {
+                    range = this.getRangeAt(0);
+                    range["set" + (isStart ? "Start" : "End")](node, offset);
+                } else {
+                    range = api.createRange(this.win.document);
+                    range.setStartAndEnd(node, offset);
+                }
+                this.setSingleRange(range, this.isBackward());
+            };
+        }
+
+        selProto.setStart = createStartOrEndSetter(true);
+        selProto.setEnd = createStartOrEndSetter(false);
+
+        // Add select() method to Range prototype. Any existing selection will be removed.
+        api.rangePrototype.select = function(direction) {
+            getSelection( this.getDocument() ).setSingleRange(this, direction);
+        };
+
+        selProto.changeEachRange = function(func) {
+            var ranges = [];
+            var backward = this.isBackward();
+
+            this.eachRange(function(range) {
+                func(range);
+                ranges.push(range);
+            });
+
+            this.removeAllRanges();
+            if (backward && ranges.length == 1) {
+                this.addRange(ranges[0], "backward");
+            } else {
+                this.setRanges(ranges);
+            }
+        };
+
+        selProto.containsNode = function(node, allowPartial) {
+            return this.eachRange( function(range) {
+                return range.containsNode(node, allowPartial);
+            }, true ) || false;
+        };
+
+        selProto.getBookmark = function(containerNode) {
+            return {
+                backward: this.isBackward(),
+                rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
+            };
+        };
+
+        selProto.moveToBookmark = function(bookmark) {
+            var selRanges = [];
+            for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
+                range = api.createRange(this.win);
+                range.moveToBookmark(rangeBookmark);
+                selRanges.push(range);
+            }
+            if (bookmark.backward) {
+                this.setSingleRange(selRanges[0], "backward");
+            } else {
+                this.setRanges(selRanges);
+            }
+        };
+
+        selProto.saveRanges = function() {
+            return {
+                backward: this.isBackward(),
+                ranges: this.callMethodOnEachRange("cloneRange")
+            };
+        };
+
+        selProto.restoreRanges = function(selRanges) {
+            this.removeAllRanges();
+            for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
+                this.addRange(range, (selRanges.backward && i == 0));
+            }
+        };
+
+        selProto.toHtml = function() {
+            var rangeHtmls = [];
+            this.eachRange(function(range) {
+                rangeHtmls.push( DomRange.toHtml(range) );
+            });
+            return rangeHtmls.join("");
+        };
+
+        if (features.implementsTextRange) {
+            selProto.getNativeTextRange = function() {
+                var sel, textRange;
+                if ( (sel = this.docSelection) ) {
+                    var range = sel.createRange();
+                    if (isTextRange(range)) {
+                        return range;
+                    } else {
+                        throw module.createError("getNativeTextRange: selection is a control selection");
+                    }
+                } else if (this.rangeCount > 0) {
+                    return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
+                } else {
+                    throw module.createError("getNativeTextRange: selection contains no range");
+                }
+            };
+        }
+
+        function inspect(sel) {
+            var rangeInspects = [];
+            var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
+            var focus = new DomPosition(sel.focusNode, sel.focusOffset);
+            var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
+
+            if (typeof sel.rangeCount != "undefined") {
+                for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                    rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
+                }
+            }
+            return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
+                    ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
+        }
+
+        selProto.getName = function() {
+            return "WrappedSelection";
+        };
+
+        selProto.inspect = function() {
+            return inspect(this);
+        };
+
+        selProto.detach = function() {
+            actOnCachedSelection(this.win, "delete");
+            deleteProperties(this);
+        };
+
+        WrappedSelection.detachAll = function() {
+            actOnCachedSelection(null, "deleteAll");
+        };
+
+        WrappedSelection.inspect = inspect;
+        WrappedSelection.isDirectionBackward = isDirectionBackward;
+
+        api.Selection = WrappedSelection;
+
+        api.selectionPrototype = selProto;
+
+        api.addShimListener(function(win) {
+            if (typeof win.getSelection == "undefined") {
+                win.getSelection = function() {
+                    return getSelection(win);
+                };
+            }
+            win = null;
+        });
+    });
+    
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Wait for document to load before initializing
+    var docReady = false;
+
+    var loadHandler = function(e) {
+        if (!docReady) {
+            docReady = true;
+            if (!api.initialized && api.config.autoInitialize) {
+                init();
+            }
+        }
+    };
+
+    if (isBrowser) {
+        // Test whether the document has already been loaded and initialize immediately if so
+        if (document.readyState == "complete") {
+            loadHandler();
+        } else {
+            if (isHostMethod(document, "addEventListener")) {
+                document.addEventListener("DOMContentLoaded", loadHandler, false);
+            }
+
+            // Add a fallback in case the DOMContentLoaded event isn't supported
+            addListener(window, "load", loadHandler);
+        }
+    }
+
+    return api;
+}, this);
\ No newline at end of file
diff --git a/Android/folioreader/src/main/assets/js/rangy-highlighter.js b/Android/folioreader/src/main/assets/js/rangy-highlighter.js
new file mode 100755 (executable)
index 0000000..ea5ef6f
--- /dev/null
@@ -0,0 +1,627 @@
+/**
+ * Highlighter module for Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core, ClassApplier and optionally TextRange modules.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+(function(factory, root) {
+    if (typeof define == "function" && define.amd) {
+        // AMD. Register as an anonymous module with a dependency on Rangy.
+        define(["./rangy-core"], factory);
+    } else if (typeof module != "undefined" && typeof exports == "object") {
+        // Node/CommonJS style
+        module.exports = factory( require("rangy") );
+    } else {
+        // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
+        factory(root.rangy);
+    }
+})(function(rangy) {
+    rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) {
+        var dom = api.dom;
+        var contains = dom.arrayContains;
+        var getBody = dom.getBody;
+        var createOptions = api.util.createOptions;
+        var forEach = api.util.forEach;
+        var nextHighlightId = 1;
+
+        // Puts highlights in order, last in document first.
+        function compareHighlights(h1, h2) {
+            return h1.characterRange.start - h2.characterRange.start;
+        }
+
+        function getContainerElement(doc, id) {
+            return id ? doc.getElementById(id) : getBody(doc);
+        }
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        var highlighterTypes = {};
+
+        function HighlighterType(type, converterCreator) {
+            this.type = type;
+            this.converterCreator = converterCreator;
+        }
+
+        HighlighterType.prototype.create = function() {
+            var converter = this.converterCreator();
+            converter.type = this.type;
+            return converter;
+        };
+
+        function registerHighlighterType(type, converterCreator) {
+            highlighterTypes[type] = new HighlighterType(type, converterCreator);
+        }
+
+        function getConverter(type) {
+            var highlighterType = highlighterTypes[type];
+            if (highlighterType instanceof HighlighterType) {
+                return highlighterType.create();
+            } else {
+                throw new Error("Highlighter type '" + type + "' is not valid");
+            }
+        }
+
+        api.registerHighlighterType = registerHighlighterType;
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        function CharacterRange(start, end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        CharacterRange.prototype = {
+            intersects: function(charRange) {
+                return this.start < charRange.end && this.end > charRange.start;
+            },
+
+            isContiguousWith: function(charRange) {
+                return this.start == charRange.end || this.end == charRange.start;
+            },
+
+            union: function(charRange) {
+                return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end));
+            },
+
+            intersection: function(charRange) {
+                return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end));
+            },
+
+            getComplements: function(charRange) {
+                var ranges = [];
+                if (this.start >= charRange.start) {
+                    if (this.end <= charRange.end) {
+                        return [];
+                    }
+                    ranges.push(new CharacterRange(charRange.end, this.end));
+                } else {
+                    ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start)));
+                    if (this.end > charRange.end) {
+                        ranges.push(new CharacterRange(charRange.end, this.end));
+                    }
+                }
+                return ranges;
+            },
+
+            toString: function() {
+                return "[CharacterRange(" + this.start + ", " + this.end + ")]";
+            }
+        };
+
+        CharacterRange.fromCharacterRange = function(charRange) {
+            return new CharacterRange(charRange.start, charRange.end);
+        };
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        var textContentConverter = {
+            rangeToCharacterRange: function(range, containerNode) {
+                var bookmark = range.getBookmark(containerNode);
+                return new CharacterRange(bookmark.start, bookmark.end);
+            },
+
+            characterRangeToRange: function(doc, characterRange, containerNode) {
+                var range = api.createRange(doc);
+                range.moveToBookmark({
+                    start: characterRange.start,
+                    end: characterRange.end,
+                    containerNode: containerNode
+                });
+
+                return range;
+            },
+
+            serializeSelection: function(selection, containerNode) {
+                var ranges = selection.getAllRanges(), rangeCount = ranges.length;
+                var rangeInfos = [];
+
+                var backward = rangeCount == 1 && selection.isBackward();
+
+                for (var i = 0, len = ranges.length; i < len; ++i) {
+                    rangeInfos[i] = {
+                        characterRange: this.rangeToCharacterRange(ranges[i], containerNode),
+                        backward: backward
+                    };
+                }
+
+                return rangeInfos;
+            },
+
+            restoreSelection: function(selection, savedSelection, containerNode) {
+                selection.removeAllRanges();
+                var doc = selection.win.document;
+                for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) {
+                    rangeInfo = savedSelection[i];
+                    characterRange = rangeInfo.characterRange;
+                    range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode);
+                    selection.addRange(range, rangeInfo.backward);
+                }
+            }
+        };
+
+        registerHighlighterType("textContent", function() {
+            return textContentConverter;
+        });
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // Lazily load the TextRange-based converter so that the dependency is only checked when required.
+        registerHighlighterType("TextRange", (function() {
+            var converter;
+
+            return function() {
+                if (!converter) {
+                    // Test that textRangeModule exists and is supported
+                    var textRangeModule = api.modules.TextRange;
+                    if (!textRangeModule) {
+                        throw new Error("TextRange module is missing.");
+                    } else if (!textRangeModule.supported) {
+                        throw new Error("TextRange module is present but not supported.");
+                    }
+
+                    converter = {
+                        rangeToCharacterRange: function(range, containerNode) {
+                            return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) );
+                        },
+
+                        characterRangeToRange: function(doc, characterRange, containerNode) {
+                            var range = api.createRange(doc);
+                            range.selectCharacters(containerNode, characterRange.start, characterRange.end);
+                            return range;
+                        },
+
+                        serializeSelection: function(selection, containerNode) {
+                            return selection.saveCharacterRanges(containerNode);
+                        },
+
+                        restoreSelection: function(selection, savedSelection, containerNode) {
+                            selection.restoreCharacterRanges(containerNode, savedSelection);
+                        }
+                    };
+                }
+
+                return converter;
+            };
+        })());
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) {
+            if (id) {
+                this.id = id;
+                nextHighlightId = Math.max(nextHighlightId, id + 1);
+            } else {
+                this.id = nextHighlightId++;
+            }
+            this.characterRange = characterRange;
+            this.doc = doc;
+            this.classApplier = classApplier;
+            this.converter = converter;
+            this.containerElementId = containerElementId || null;
+            this.applied = false;
+        }
+
+        Highlight.prototype = {
+            getContainerElement: function() {
+                return getContainerElement(this.doc, this.containerElementId);
+            },
+
+            getRange: function() {
+                return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement());
+            },
+
+            fromRange: function(range) {
+                this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement());
+            },
+
+            getText: function() {
+                return this.getRange().toString();
+            },
+
+            containsElement: function(el) {
+                return this.getRange().containsNodeContents(el.firstChild);
+            },
+
+            unapply: function() {
+                this.classApplier.undoToRange(this.getRange());
+                this.applied = false;
+            },
+
+            apply: function(serializedHighlight) {
+                this.classApplier.applyToRange(this.getRange() ,null, serializedHighlight);
+                this.applied = true;
+            },
+
+            getHighlightElements: function() {
+                return this.classApplier.getElementsWithClassIntersectingRange(this.getRange());
+            },
+
+            toString: function() {
+                return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " +
+                    this.characterRange.start + " - " + this.characterRange.end + ")]";
+            }
+        };
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        function Highlighter(doc, type) {
+            type = type || "textContent";
+            this.doc = doc || document;
+            this.classAppliers = {};
+            this.highlights = [];
+            this.converter = getConverter(type);
+        }
+
+        Highlighter.prototype = {
+            addClassApplier: function(classApplier) {
+                this.classAppliers[classApplier.className] = classApplier;
+            },
+
+            getHighlightForElement: function(el) {
+                var highlights = this.highlights;
+                for (var i = 0, len = highlights.length; i < len; ++i) {
+                    if (highlights[i].containsElement(el)) {
+                        return highlights[i];
+                    }
+                }
+                return null;
+            },
+
+            removeHighlights: function(highlights) {
+                for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) {
+                    highlight = this.highlights[i];
+                    if (contains(highlights, highlight)) {
+                        highlight.unapply();
+                        this.highlights.splice(i--, 1);
+                    }
+                }
+            },
+
+            removeAllHighlights: function() {
+                this.removeHighlights(this.highlights);
+            },
+
+            getIntersectingHighlights: function(ranges) {
+                // Test each range against each of the highlighted ranges to see whether they overlap
+                var intersectingHighlights = [], highlights = this.highlights;
+                forEach(ranges, function(range) {
+                    //var selCharRange = converter.rangeToCharacterRange(range);
+                    forEach(highlights, function(highlight) {
+                        if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) {
+                            intersectingHighlights.push(highlight);
+                        }
+                    });
+                });
+
+                return intersectingHighlights;
+            },
+
+            highlightCharacterRanges: function(className, charRanges, options) {
+                var i, len, j;
+                var highlights = this.highlights;
+                var converter = this.converter;
+                var doc = this.doc;
+                var highlightsToRemove = [];
+                var classApplier = className ? this.classAppliers[className] : null;
+
+                options = createOptions(options, {
+                    containerElementId: null,
+                    exclusive: true
+                });
+
+                var containerElementId = options.containerElementId;
+                var exclusive = options.exclusive;
+
+                var containerElement, containerElementRange, containerElementCharRange;
+                if (containerElementId) {
+                    containerElement = this.doc.getElementById(containerElementId);
+                    if (containerElement) {
+                        containerElementRange = api.createRange(this.doc);
+                        containerElementRange.selectNodeContents(containerElement);
+                        containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length);
+                    }
+                }
+
+                var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight;
+
+                for (i = 0, len = charRanges.length; i < len; ++i) {
+                    charRange = charRanges[i];
+                    highlightsToKeep = [];
+
+                    // Restrict character range to container element, if it exists
+                    if (containerElementCharRange) {
+                        charRange = charRange.intersection(containerElementCharRange);
+                    }
+
+                    // Ignore empty ranges
+                    if (charRange.start == charRange.end) {
+                        continue;
+                    }
+
+                    // Check for intersection with existing highlights. For each intersection, create a new highlight
+                    // which is the union of the highlight range and the selected range
+                    for (j = 0; j < highlights.length; ++j) {
+                        removeHighlight = false;
+
+                        if (containerElementId == highlights[j].containerElementId) {
+                            highlightCharRange = highlights[j].characterRange;
+                            isSameClassApplier = (classApplier == highlights[j].classApplier);
+                            splitHighlight = !isSameClassApplier && exclusive;
+
+                            // Replace the existing highlight if it needs to be:
+                            //  1. merged (isSameClassApplier)
+                            //  2. partially or entirely erased (className === null)
+                            //  3. partially or entirely replaced (isSameClassApplier == false && exclusive == true)
+                            if (    (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) &&
+                                    (isSameClassApplier || splitHighlight) ) {
+
+                                // Remove existing highlights, keeping the unselected parts
+                                if (splitHighlight) {
+                                    forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) {
+                                        highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) );
+                                    });
+                                }
+
+                                removeHighlight = true;
+                                if (isSameClassApplier) {
+                                    charRange = highlightCharRange.union(charRange);
+                                }
+                            }
+                        }
+
+                        if (removeHighlight) {
+                            highlightsToRemove.push(highlights[j]);
+                            highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId);
+                        } else {
+                            highlightsToKeep.push(highlights[j]);
+                        }
+                    }
+
+                    // Add new range
+                    if (classApplier) {
+                        highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId));
+                    }
+                    var  oldHighlights = this.serialize(null).split("|");
+
+                    this.highlights = highlights = highlightsToKeep;
+                }
+
+                // Remove the old highlights
+                forEach(highlightsToRemove, function(highlightToRemove) {
+                    highlightToRemove.unapply();
+                });
+
+
+                var serializedHighlights = this.serialize(null).split("|");
+                var highlightStr = array_diff(oldHighlights, serializedHighlights)[0];
+
+                // Apply new highlights
+                var newHighlights = [];
+                forEach(highlights, function(highlight) {
+                    if (!highlight.applied) {
+                        highlight.apply(highlightStr);
+                        newHighlights.push(highlight);
+                    }
+                });
+
+                return newHighlights;
+            },
+
+            highlightRanges: function(className, ranges, options) {
+                var selCharRanges = [];
+                var converter = this.converter;
+
+                options = createOptions(options, {
+                    containerElement: null,
+                    exclusive: true
+                });
+
+                var containerElement = options.containerElement;
+                var containerElementId = containerElement ? containerElement.id : null;
+                var containerElementRange;
+                if (containerElement) {
+                    containerElementRange = api.createRange(containerElement);
+                    containerElementRange.selectNodeContents(containerElement);
+                }
+
+                forEach(ranges, function(range) {
+                    var scopedRange = containerElement ? containerElementRange.intersection(range) : range;
+                    selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) );
+                });
+
+                return this.highlightCharacterRanges(className, selCharRanges, {
+                    containerElementId: containerElementId,
+                    exclusive: options.exclusive
+                });
+            },
+
+            highlightSelection: function(className, options) {
+                var converter = this.converter;
+                var classApplier = className ? this.classAppliers[className] : false;
+
+                options = createOptions(options, {
+                    containerElementId: null,
+                    selection: api.getSelection(this.doc),
+                    exclusive: true
+                });
+
+                var containerElementId = options.containerElementId;
+                var exclusive = options.exclusive;
+                var selection = options.selection;
+                var doc = selection.win.document;
+                var containerElement = getContainerElement(doc, containerElementId);
+
+                if (!classApplier && className !== false) {
+                    throw new Error("No class applier found for class '" + className + "'");
+                }
+
+                // Store the existing selection as character ranges
+                var serializedSelection = converter.serializeSelection(selection, containerElement);
+
+                // Create an array of selected character ranges
+                var selCharRanges = [];
+                forEach(serializedSelection, function(rangeInfo) {
+                    selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) );
+                });
+
+                var newHighlights = this.highlightCharacterRanges(className, selCharRanges, {
+                    containerElementId: containerElementId,
+                    exclusive: exclusive
+                });
+
+                // Restore selection
+                converter.restoreSelection(selection, serializedSelection, containerElement);
+
+                return newHighlights;
+            },
+
+            unhighlightSelection: function(selection) {
+                selection = selection || api.getSelection(this.doc);
+                var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() );
+                this.removeHighlights(intersectingHighlights);
+                selection.removeAllRanges();
+                return intersectingHighlights;
+            },
+
+            getHighlightsInSelection: function(selection) {
+                selection = selection || api.getSelection(this.doc);
+                return this.getIntersectingHighlights(selection.getAllRanges());
+            },
+
+            selectionOverlapsHighlight: function(selection) {
+                return this.getHighlightsInSelection(selection).length > 0;
+            },
+
+            serialize: function(options) {
+                var highlighter = this;
+                var highlights = highlighter.highlights;
+                var serializedType, serializedHighlights, convertType, serializationConverter;
+
+                highlights.sort(compareHighlights);
+                options = createOptions(options, {
+                    serializeHighlightText: false,
+                    type: highlighter.converter.type
+                });
+
+                serializedType = options.type;
+                convertType = (serializedType != highlighter.converter.type);
+
+                if (convertType) {
+                    serializationConverter = getConverter(serializedType);
+                }
+
+                serializedHighlights = ["type:" + serializedType];
+
+                forEach(highlights, function(highlight) {
+                    var characterRange = highlight.characterRange;
+                    var containerElement;
+
+                    // Convert to the current Highlighter's type, if different from the serialization type
+                    if (convertType) {
+                        containerElement = highlight.getContainerElement();
+                        characterRange = serializationConverter.rangeToCharacterRange(
+                            highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement),
+                            containerElement
+                        );
+                    }
+
+                    var parts = [
+                        characterRange.start,
+                        characterRange.end,
+                        highlight.id,
+                        highlight.classApplier.className,
+                        highlight.containerElementId
+                    ];
+
+                    if (options.serializeHighlightText) {
+                        parts.push(highlight.getText());
+                    }
+                    serializedHighlights.push( parts.join("$") );
+                });
+                return serializedHighlights.join("|");
+            },
+
+            deserialize: function(serialized) {
+                var serializedHighlights = serialized.split("|");
+                var highlights = [];
+
+                var firstHighlight = serializedHighlights[0];
+                var regexResult;
+                var serializationType, serializationConverter, convertType = false;
+                if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) {
+                    serializationType = regexResult[1];
+                    if (serializationType != this.converter.type) {
+                        serializationConverter = getConverter(serializationType);
+                        convertType = true;
+                    }
+                    serializedHighlights.shift();
+                } else {
+                    throw new Error("Serialized highlights are invalid.");
+                }
+
+                var classApplier, highlight, characterRange, containerElementId, containerElement;
+
+                for (var i = serializedHighlights.length, parts; i-- > 0; ) {
+                    parts = serializedHighlights[i].split("$");
+                    characterRange = new CharacterRange(+parts[0], +parts[1]);
+                    containerElementId = parts[4] || null;
+
+                    // Convert to the current Highlighter's type, if different from the serialization type
+                    if (convertType) {
+                        containerElement = getContainerElement(this.doc, containerElementId);
+                        characterRange = this.converter.rangeToCharacterRange(
+                            serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement),
+                            containerElement
+                        );
+                    }
+
+                    classApplier = this.classAppliers[ parts[3] ];
+
+                    if (!classApplier) {
+                        throw new Error("No class applier found for class '" + parts[3] + "'");
+                    }
+
+                    highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId);
+
+
+                    highlight.apply(serializedHighlights[i]);
+                    highlights.push(highlight);
+                }
+                this.highlights = highlights;
+            }
+        };
+
+        api.Highlighter = Highlighter;
+
+        api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) {
+            return new Highlighter(doc, rangeCharacterOffsetConverterType);
+        };
+    });
+    
+    return rangy;
+}, this);
diff --git a/Android/folioreader/src/main/assets/js/rangy-serializer.js b/Android/folioreader/src/main/assets/js/rangy-serializer.js
new file mode 100755 (executable)
index 0000000..f2d5759
--- /dev/null
@@ -0,0 +1,314 @@
+/**
+ * Serializer module for Rangy.
+ * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
+ * cookie or local storage and restore it on the user's next visit to the same page.
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+(function(factory, root) {
+    if (typeof define == "function" && define.amd) {
+        // AMD. Register as an anonymous module with a dependency on Rangy.
+        define(["./rangy-core"], factory);
+    } else if (typeof module != "undefined" && typeof exports == "object") {
+        // Node/CommonJS style
+        module.exports = factory( require("rangy") );
+    } else {
+        // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
+        factory(root.rangy);
+    }
+})(function(rangy) {
+    rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) {
+        var UNDEF = "undefined";
+        var util = api.util;
+
+        // encodeURIComponent and decodeURIComponent are required for cookie handling
+        if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
+            module.fail("encodeURIComponent and/or decodeURIComponent method is missing");
+        }
+
+        // Checksum for checking whether range can be serialized
+        var crc32 = (function() {
+            function utf8encode(str) {
+                var utf8CharCodes = [];
+
+                for (var i = 0, len = str.length, c; i < len; ++i) {
+                    c = str.charCodeAt(i);
+                    if (c < 128) {
+                        utf8CharCodes.push(c);
+                    } else if (c < 2048) {
+                        utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
+                    } else {
+                        utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
+                    }
+                }
+                return utf8CharCodes;
+            }
+
+            var cachedCrcTable = null;
+
+            function buildCRCTable() {
+                var table = [];
+                for (var i = 0, j, crc; i < 256; ++i) {
+                    crc = i;
+                    j = 8;
+                    while (j--) {
+                        if ((crc & 1) == 1) {
+                            crc = (crc >>> 1) ^ 0xEDB88320;
+                        } else {
+                            crc >>>= 1;
+                        }
+                    }
+                    table[i] = crc >>> 0;
+                }
+                return table;
+            }
+
+            function getCrcTable() {
+                if (!cachedCrcTable) {
+                    cachedCrcTable = buildCRCTable();
+                }
+                return cachedCrcTable;
+            }
+
+            return function(str) {
+                var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
+                for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
+                    y = (crc ^ utf8CharCodes[i]) & 0xFF;
+                    crc = (crc >>> 8) ^ crcTable[y];
+                }
+                return (crc ^ -1) >>> 0;
+            };
+        })();
+
+        var dom = api.dom;
+
+        function escapeTextForHtml(str) {
+            return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
+        }
+
+        function nodeToInfoString(node, infoParts) {
+            infoParts = infoParts || [];
+            var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
+            var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
+            var start = "", end = "";
+            switch (nodeType) {
+                case 3: // Text node
+                    start = escapeTextForHtml(node.nodeValue);
+                    break;
+                case 8: // Comment
+                    start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
+                    break;
+                default:
+                    start = "<" + nodeInfo + ">";
+                    end = "</>";
+                    break;
+            }
+            if (start) {
+                infoParts.push(start);
+            }
+            for (var i = 0; i < childCount; ++i) {
+                nodeToInfoString(children[i], infoParts);
+            }
+            if (end) {
+                infoParts.push(end);
+            }
+            return infoParts;
+        }
+
+        // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
+        // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
+        // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
+        // innerHTML whenever the user changes an input within the element.
+        function getElementChecksum(el) {
+            var info = nodeToInfoString(el).join("");
+            return crc32(info).toString(16);
+        }
+
+        function serializePosition(node, offset, rootNode) {
+            var pathParts = [], n = node;
+            rootNode = rootNode || dom.getDocument(node).documentElement;
+            while (n && n != rootNode) {
+                pathParts.push(dom.getNodeIndex(n, true));
+                n = n.parentNode;
+            }
+            return pathParts.join("/") + ":" + offset;
+        }
+
+        function deserializePosition(serialized, rootNode, doc) {
+            if (!rootNode) {
+                rootNode = (doc || document).documentElement;
+            }
+            var parts = serialized.split(":");
+            var node = rootNode;
+            var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex;
+
+            while (i--) {
+                nodeIndex = parseInt(nodeIndices[i], 10);
+                if (nodeIndex < node.childNodes.length) {
+                    node = node.childNodes[nodeIndex];
+                } else {
+                    throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) +
+                            " has no child with index " + nodeIndex + ", " + i);
+                }
+            }
+
+            return new dom.DomPosition(node, parseInt(parts[1], 10));
+        }
+
+        function serializeRange(range, omitChecksum, rootNode) {
+            rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
+            if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) {
+                throw module.createError("serializeRange(): range " + range.inspect() +
+                    " is not wholly contained within specified root node " + dom.inspectNode(rootNode));
+            }
+            var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
+                serializePosition(range.endContainer, range.endOffset, rootNode);
+            if (!omitChecksum) {
+                serialized += "{" + getElementChecksum(rootNode) + "}";
+            }
+            return serialized;
+        }
+
+        var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/;
+
+        function deserializeRange(serialized, rootNode, doc) {
+            if (rootNode) {
+                doc = doc || dom.getDocument(rootNode);
+            } else {
+                doc = doc || document;
+                rootNode = doc.documentElement;
+            }
+            var result = deserializeRegex.exec(serialized);
+            var checksum = result[4];
+            if (checksum) {
+                var rootNodeChecksum = getElementChecksum(rootNode);
+                if (checksum !== rootNodeChecksum) {
+                    throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum +
+                        ") and target root node (" + rootNodeChecksum + ") do not match");
+                }
+            }
+            var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
+            var range = api.createRange(doc);
+            range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
+            return range;
+        }
+
+        function canDeserializeRange(serialized, rootNode, doc) {
+            if (!rootNode) {
+                rootNode = (doc || document).documentElement;
+            }
+            var result = deserializeRegex.exec(serialized);
+            var checksum = result[3];
+            return !checksum || checksum === getElementChecksum(rootNode);
+        }
+
+        function serializeSelection(selection, omitChecksum, rootNode) {
+            selection = api.getSelection(selection);
+            var ranges = selection.getAllRanges(), serializedRanges = [];
+            for (var i = 0, len = ranges.length; i < len; ++i) {
+                serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
+            }
+            return serializedRanges.join("|");
+        }
+
+        function deserializeSelection(serialized, rootNode, win) {
+            if (rootNode) {
+                win = win || dom.getWindow(rootNode);
+            } else {
+                win = win || window;
+                rootNode = win.document.documentElement;
+            }
+            var serializedRanges = serialized.split("|");
+            var sel = api.getSelection(win);
+            var ranges = [];
+
+            for (var i = 0, len = serializedRanges.length; i < len; ++i) {
+                ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
+            }
+            sel.setRanges(ranges);
+
+            return sel;
+        }
+
+        function canDeserializeSelection(serialized, rootNode, win) {
+            var doc;
+            if (rootNode) {
+                doc = win ? win.document : dom.getDocument(rootNode);
+            } else {
+                win = win || window;
+                rootNode = win.document.documentElement;
+            }
+            var serializedRanges = serialized.split("|");
+
+            for (var i = 0, len = serializedRanges.length; i < len; ++i) {
+                if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        var cookieName = "rangySerializedSelection";
+
+        function getSerializedSelectionFromCookie(cookie) {
+            var parts = cookie.split(/[;,]/);
+            for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
+                nameVal = parts[i].split("=");
+                if (nameVal[0].replace(/^\s+/, "") == cookieName) {
+                    val = nameVal[1];
+                    if (val) {
+                        return decodeURIComponent(val.replace(/\s+$/, ""));
+                    }
+                }
+            }
+            return null;
+        }
+
+        function restoreSelectionFromCookie(win) {
+            win = win || window;
+            var serialized = getSerializedSelectionFromCookie(win.document.cookie);
+            if (serialized) {
+                deserializeSelection(serialized, win.doc);
+            }
+        }
+
+        function saveSelectionCookie(win, props) {
+            win = win || window;
+            props = (typeof props == "object") ? props : {};
+            var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
+            var path = props.path ? ";path=" + props.path : "";
+            var domain = props.domain ? ";domain=" + props.domain : "";
+            var secure = props.secure ? ";secure" : "";
+            var serialized = serializeSelection(api.getSelection(win));
+            win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
+        }
+
+        util.extend(api, {
+            serializePosition: serializePosition,
+            deserializePosition: deserializePosition,
+            serializeRange: serializeRange,
+            deserializeRange: deserializeRange,
+            canDeserializeRange: canDeserializeRange,
+            serializeSelection: serializeSelection,
+            deserializeSelection: deserializeSelection,
+            canDeserializeSelection: canDeserializeSelection,
+            restoreSelectionFromCookie: restoreSelectionFromCookie,
+            saveSelectionCookie: saveSelectionCookie,
+            getElementChecksum: getElementChecksum,
+            nodeToInfoString: nodeToInfoString
+        });
+
+        util.crc32 = crc32;
+    });
+    
+    return rangy;
+}, this);
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/Config.java b/Android/folioreader/src/main/java/com/folioreader/Config.java
new file mode 100755 (executable)
index 0000000..321d432
--- /dev/null
@@ -0,0 +1,279 @@
+package com.folioreader;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.json.JSONObject;
+
+/**
+ * Created by mahavir on 4/12/16.
+ */
+public class Config implements Parcelable {
+       public static final String INTENT_CONFIG = "config";
+       public static final String CONFIG_FONT = "font";
+       public static final String CONFIG_FONT_SIZE = "font_size";
+       public static final String CONFIG_MARGIN_SIZE = "margin_size";
+       public static final String CONFIG_INTERLINE_SIZE = "interline_size";
+       public static final String CONFIG_IS_NIGHTMODE = "is_night_mode";
+       public static final String CONFIG_THEMECOLOR = "theme_color";
+       public static final String CONFIG_ICON_COLOR = "icon_color";
+       public static final String CONFIG_TOOLBAR_COLOR = "toolbar_color";
+       public static final String CONFIG_IS_TTS = "is_tts";
+       public static final String INTENT_PORT = "port";
+       private int font;
+       private int fontSize;
+       private int marginSize;
+       private int interlineSize;
+       private boolean nightMode;
+       private int themeColor;
+       private int iconColor;
+       private int toolbarColor;
+       private boolean showTts;
+
+       //    public Config(int font, int fontSize, boolean nightMode, int themeColor, int iconColor, int toolbarColor, boolean showTts) {
+       //        this.font = font;
+       //        this.fontSize = fontSize;
+       //        this.nightMode = nightMode;
+       //        this.themeColor = themeColor;
+       //        this.iconColor = iconColor;
+       //        this.toolbarColor = toolbarColor;
+       //        this.showTts = showTts;
+       //    }
+
+       private Config(ConfigBuilder configBuilder) {
+               font = configBuilder.mFont;
+               fontSize = configBuilder.mFontSize;
+               marginSize = configBuilder.mMarginSize;
+               interlineSize = configBuilder.mInterlineSize;
+               nightMode = configBuilder.mNightMode;
+               themeColor = configBuilder.mThemeColor;
+               iconColor = configBuilder.iconColor;
+               toolbarColor = configBuilder.toolbarColor;
+               showTts = configBuilder.mShowTts;
+       }
+
+       public Config(JSONObject jsonObject) {
+               font = jsonObject.optInt(CONFIG_FONT);
+               fontSize = jsonObject.optInt(CONFIG_FONT_SIZE);
+               marginSize = jsonObject.optInt(CONFIG_MARGIN_SIZE);
+               interlineSize = jsonObject.optInt(CONFIG_INTERLINE_SIZE);
+               nightMode = jsonObject.optBoolean(CONFIG_IS_NIGHTMODE);
+               themeColor = jsonObject.optInt(CONFIG_THEMECOLOR);
+               iconColor = jsonObject.optInt(CONFIG_ICON_COLOR);
+               toolbarColor = jsonObject.optInt(CONFIG_TOOLBAR_COLOR);
+               showTts = jsonObject.optBoolean(CONFIG_IS_TTS);
+       }
+
+       private Config() {
+               fontSize = 2;
+               marginSize = 1;
+               interlineSize = 1;
+               font = 3;
+               nightMode = false;
+               themeColor = R.color.app_green;
+               showTts = true;
+       }
+
+       private Config(Parcel in) {
+               readFromParcel(in);
+       }
+
+
+       public int getFont() {
+               return font;
+       }
+
+       public void setFont(int font) {
+               this.font = font;
+       }
+
+       public int getFontSize() {
+               return fontSize;
+       }
+
+       public void setFontSize(int fontSize) {
+               this.fontSize = fontSize;
+       }
+
+       public int getMarginSize() {
+               return marginSize;
+       }
+
+       public void setMarginSize(int marginSize) {
+               this.marginSize = marginSize;
+       }
+
+       public int getInterlineSize() {
+               return interlineSize;
+       }
+
+       public void setInterlineSize(int interlineSize) {
+               this.interlineSize = interlineSize;
+       }
+
+       public boolean isNightMode() {
+               return nightMode;
+       }
+
+       public void setNightMode(boolean nightMode) {
+               this.nightMode = nightMode;
+       }
+
+
+       public int getThemeColor() {
+               return themeColor;
+       }
+
+       public void setThemeColor(int themeColor) {
+               this.themeColor = themeColor;
+       }
+
+       public boolean isShowTts() {
+               return showTts;
+       }
+
+       public void setShowTts(boolean showTts) {
+               this.showTts = showTts;
+       }
+
+       public int getIconColor() {
+               return iconColor;
+       }
+
+       public int getToolbarColor() {
+               return toolbarColor;
+       }
+
+       @Override
+
+       public boolean equals(Object o) {
+               if (this == o) {
+                       return true;
+               }
+               if (!(o instanceof Config)) {
+                       return false;
+               }
+
+               Config config = (Config) o;
+
+               return font == config.font && fontSize == config.fontSize && marginSize == config.marginSize && interlineSize == config.interlineSize && nightMode == config.nightMode &&
+                               toolbarColor == config.toolbarColor && iconColor == config.iconColor;
+       }
+
+       @Override
+       public int hashCode() {
+               int result = font;
+               result = 31 * result + fontSize;
+               result = 31 * result + marginSize;
+               result = 31 * result + interlineSize;
+               result = 31 * result + (nightMode ? 1 : 0);
+               return result;
+       }
+
+       @Override
+       public String toString() {
+               return "Config{"
+                               + "font=" + font
+                               + ", fontSize=" + fontSize
+                               + ", marginSize=" + marginSize
+                               + ", interlineSize=" + interlineSize
+                               + ", nightMode=" + nightMode
+                               + '}';
+       }
+
+       @Override
+       public int describeContents() {
+               return 0;
+       }
+
+       @Override
+       public void writeToParcel(Parcel dest, int flags) {
+               dest.writeInt(font);
+               dest.writeInt(fontSize);
+               dest.writeInt(marginSize);
+               dest.writeInt(interlineSize);
+               dest.writeInt(nightMode ? 1 : 0);
+               dest.writeInt(themeColor);
+               dest.writeInt(showTts ? 1 : 0);
+               dest.writeInt(toolbarColor);
+               dest.writeInt(iconColor);
+       }
+
+       private void readFromParcel(Parcel in) {
+               font = in.readInt();
+               fontSize = in.readInt();
+               marginSize = in.readInt();
+               interlineSize = in.readInt();
+               nightMode = in.readInt() == 1;
+               themeColor = in.readInt();
+               showTts = in.readInt() == 1;
+               toolbarColor = in.readInt();
+               iconColor = in.readInt();
+       }
+
+       public static final Creator<Config> CREATOR = new Creator<Config>() {
+               @Override
+               public Config createFromParcel(Parcel in) {
+                       return new Config(in);
+               }
+
+               @Override
+               public Config[] newArray(int size) {
+                       return new Config[size];
+               }
+       };
+
+       public static class ConfigBuilder {
+               private int mFont = 3;
+               private int mFontSize = 2;
+               private int mMarginSize = 1;
+               private int mInterlineSize = 1;
+               private boolean mNightMode = false;
+               private int mThemeColor = R.color.settings_icons;
+               private boolean mShowTts = true;
+               private int iconColor = R.color.toolbar_icons;
+               private int toolbarColor = R.color.toolbar_background;
+
+               public ConfigBuilder font(int font) {
+                       mFont = font;
+                       return this;
+               }
+
+               public ConfigBuilder fontSize(int fontSize) {
+                       mFontSize = fontSize;
+                       return this;
+               }
+
+               public ConfigBuilder marginSize(int marginSize) {
+                       mMarginSize = marginSize;
+                       return this;
+               }
+
+               public ConfigBuilder interlineSize(int interlineSize) {
+                       mInterlineSize = interlineSize;
+                       return this;
+               }
+
+               public ConfigBuilder nightmode(boolean nightMode) {
+                       mNightMode = nightMode;
+                       return this;
+               }
+
+               public ConfigBuilder themeColor(int themeColor) {
+                       mThemeColor = themeColor;
+                       return this;
+               }
+
+               public ConfigBuilder setShowTts(boolean showTts) {
+                       mShowTts = showTts;
+                       return this;
+               }
+
+
+               public Config build() {
+                       return new Config(this);
+               }
+       }
+}
+
+
diff --git a/Android/folioreader/src/main/java/com/folioreader/Constants.java b/Android/folioreader/src/main/java/com/folioreader/Constants.java
new file mode 100755 (executable)
index 0000000..5661b11
--- /dev/null
@@ -0,0 +1,36 @@
+package com.folioreader;\r
+\r
+import android.Manifest;\r
+\r
+/**\r
+ * Created by mobisys on 10/4/2016.\r
+ */\r
+public class Constants {\r
+    public static final String SELECTED_CHAPTER_POSITION = "selected_chapter_position";\r
+    public static final String TYPE = "type";\r
+    public static final String CHAPTER_SELECTED = "chapter_selected";\r
+    public static final String HIGHLIGHT_SELECTED = "highlight_selected";\r
+    public static final String BOOK_TITLE = "book_title";\r
+    public static final String WEBVIEW_SCROLL_POSITION = "webView_scroll_position";\r
+    public static final String VIEWPAGER_POSITION = "view_pager_position";\r
+    public static final String VIEWPAGER_COUNT = "viewpager_count";\r
+    public static final String BOOK_STATE = "book_state";\r
+    public static final int PORT_NUMBER = 8080;\r
+    public static final String LOCALHOST = "http://127.0.0.1:" + PORT_NUMBER + "/";\r
+    public static final String SELECTED_WORD = "selected_word";\r
+    public static final String DICTIONARY_BASE_URL = "http://api.pearson.com/v2/dictionaries/entries?headword=";\r
+    public static final String WIKIPEDIA_API_URL = "https://en.wikipedia.org/w/api.php?action=opensearch&namespace=0&format=json&search=";\r
+    public static final int FONT_EBGARAMOND = 1;\r
+    public static final int FONT_LATO = 2;\r
+    public static final int FONT_LORA = 3;\r
+    public static final int FONT_RALEWAY = 4;\r
+    public static final String DATE_FORMAT = "MMM dd, yyyy | HH:mm";\r
+    public static final String ASSET = "file:///android_asset/";\r
+    public static final int WRITE_EXTERNAL_STORAGE_REQUEST = 102;\r
+\r
+    public static String[] getWriteExternalStoragePerms() {\r
+        return new String[] {\r
+            Manifest.permission.WRITE_EXTERNAL_STORAGE\r
+        };\r
+    }\r
+}\r
diff --git a/Android/folioreader/src/main/java/com/folioreader/Font.java b/Android/folioreader/src/main/java/com/folioreader/Font.java
new file mode 100755 (executable)
index 0000000..5b08d46
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+* Copyright (C) 2016 Pedro Paulo de Amorim
+*
+* 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.folioreader;
+
+public class Font {
+
+    String name;
+
+    public Font(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/HighLight.java b/Android/folioreader/src/main/java/com/folioreader/model/HighLight.java
new file mode 100755 (executable)
index 0000000..60c9457
--- /dev/null
@@ -0,0 +1,74 @@
+package com.folioreader.model;
+
+import java.util.Date;
+
+/**
+ * Interface to access Highlight data.
+ *
+ * @author gautam chibde on 9/10/17.
+ */
+
+public interface HighLight {
+
+    /**
+     * Highlight action
+     */
+    enum HighLightAction {
+        NEW, DELETE, MODIFY
+    }
+
+    /**
+     * <p> Returns Book id, which can be provided to intent to folio reader, if not provided id is
+     * used from epub's dc:identifier field in metadata.
+     * <p>for reference, look here:
+     * <a href="http://www.idpf.org/epub/30/spec/epub30-publications.html#sec-package-metadata-identifiers">IDPF</a>.</p>
+     * in case identifier is not found in the epub,
+     * <a href="https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#hashCode()">hash code</a>
+     * of book title is used also if book title is not found then
+     * hash code of the book file name is used.
+     * </p>
+     */
+    String getBookId();
+
+    /**
+     * Returns Highlighted text content text content.
+     */
+    String getContent();
+
+    /**
+     * Returns Date time when highlight is created (format:- MMM dd, yyyy | HH:mm).
+     */
+    Date getDate();
+
+    /**
+     * Returns Field defines the color of the highlight.
+     */
+    String getType();
+
+    /**
+     * Returns Page index in the book taken from Epub spine reference.
+     */
+    int getPageNumber();
+
+    /**
+     * Returns href of the page from the Epub spine list.
+     */
+    String getPageId();
+
+    /**
+     * <p> Contains highlight meta data in terms of rangy format.</p>
+     * <strong>format </strong>:- start$end$id$class$containerId.
+     * <p>for reference, look here: <a href="https://github.com/timdown/rangy">rangy</a>.</p>
+     */
+    String getRangy();
+
+    /**
+     * returns Unique identifier.
+     */
+    String getUUID();
+
+    /**
+     * Returns Note linked to the highlight (optional)
+     */
+    String getNote();
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/HighlightImpl.java b/Android/folioreader/src/main/java/com/folioreader/model/HighlightImpl.java
new file mode 100755 (executable)
index 0000000..4792a79
--- /dev/null
@@ -0,0 +1,295 @@
+package com.folioreader.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Date;
+
+/**
+ * This data structure holds information about an individual highlight.
+ *
+ * @author mahavir on 5/12/16.
+ */
+
+public class HighlightImpl implements Parcelable, HighLight {
+
+    public static final String INTENT = HighlightImpl.class.getName();
+    public static final String BROADCAST_EVENT = "highlight_broadcast_event";
+
+    /**
+     * Database id
+     */
+    private int id;
+    /**
+     * <p> Book id, which can be provided to intent to folio reader, if not provided id is
+     * used from epub's dc:identifier field in metadata.
+     * <p>for reference, look here:
+     * <a href="http://www.idpf.org/epub/30/spec/epub30-publications.html#sec-package-metadata-identifiers">IDPF</a>.</p>
+     * in case identifier is not found in the epub,
+     * <a href="https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#hashCode()">hash code</a>
+     * of book title is used also if book title is not found then
+     * hash code of the book file name is used.
+     * </p>
+     */
+    private String bookId;
+    /**
+     * Highlighted text content text content.
+     */
+    private String content;
+    /**
+     * Date time when highlight is created (format:- MMM dd, yyyy | HH:mm).
+     */
+    private Date date;
+    /**
+     * Field defines the color of the highlight. {@link HighlightStyle}
+     */
+    private String type;
+    /**
+     * Page index in the book taken from Epub spine reference.
+     */
+    private int pageNumber;
+    /**
+     * href of the page from the Epub spine list.
+     */
+    private String pageId;
+    /**
+     * <p> Contains highlight meta data in terms of rangy format.</p>
+     * <strong>format </strong>:- start$end$id$class$containerId.
+     * <p>for reference, look here: <a href="https://github.com/timdown/rangy">rangy</a>.</p>
+     */
+    private String rangy;
+
+    /**
+     * Unique identifier for a highlight for sync across devices.
+     * <p>for reference, look here:
+     * <a href = "https://docs.oracle.com/javase/7/docs/api/java/util/UUID.html#toString()">UUID</a>.</p>
+     */
+    private String uuid;
+
+    /**
+     * Note linked to the highlight (optional)
+     */
+    private String note;
+
+    public enum HighlightStyle {
+        Yellow,
+        Green,
+        Blue,
+        Pink,
+        Underline,
+        TextColor,
+        DottetUnderline,
+        Normal;
+
+        /**
+         * Return CSS class for HighlightStyle.
+         */
+        public static String classForStyle(HighlightStyle style) {
+            switch (style) {
+                case Yellow:
+                    return "yellow";
+                case Green:
+                    return "green";
+                case Blue:
+                    return "blue";
+                case Pink:
+                    return "pink";
+                case Underline:
+                    return "underline";
+                case DottetUnderline:
+                    return "mediaOverlayStyle1";
+                case TextColor:
+                    return "mediaOverlayStyle2";
+                default:
+                    return "mediaOverlayStyle0";
+
+            }
+        }
+    }
+
+    public HighlightImpl(int id, String bookId, String content, Date date, String type,
+                         int pageNumber, String pageId,
+                         String rangy, String note, String uuid) {
+        this.id = id;
+        this.bookId = bookId;
+        this.content = content;
+        this.date = date;
+        this.type = type;
+        this.pageNumber = pageNumber;
+        this.pageId = pageId;
+        this.rangy = rangy;
+        this.note = note;
+        this.uuid = uuid;
+    }
+
+    public HighlightImpl() {
+    }
+
+    protected HighlightImpl(Parcel in) {
+        readFromParcel(in);
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getBookId() {
+        return bookId;
+    }
+
+    public void setBookId(String bookId) {
+        this.bookId = bookId;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public Date getDate() {
+        return date;
+    }
+
+    public void setDate(Date date) {
+        this.date = date;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public String getPageId() {
+        return pageId;
+    }
+
+    public void setPageId(String pageId) {
+        this.pageId = pageId;
+    }
+
+    public String getRangy() {
+        return rangy;
+    }
+
+    public void setRangy(String rangy) {
+        this.rangy = rangy;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public int getPageNumber() {
+        return pageNumber;
+    }
+
+    public void setPageNumber(int pageNumber) {
+        this.pageNumber = pageNumber;
+    }
+
+    public String getNote() {
+        return note;
+    }
+
+    public String getUUID() {
+        return uuid;
+    }
+
+    public void setUUID(String uuid) {
+        this.uuid = uuid;
+    }
+
+    public void setNote(String note) {
+        this.note = note;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        HighlightImpl highlightImpl = (HighlightImpl) o;
+
+        return id == highlightImpl.id
+                && (bookId != null ? bookId.equals(highlightImpl.bookId) : highlightImpl.bookId == null
+                && (content != null ? content.equals(highlightImpl.content) : highlightImpl.content == null
+                && (date != null ? date.equals(highlightImpl.date) : highlightImpl.date == null
+                && (type != null ? type.equals(highlightImpl.type) : highlightImpl.type == null))));
+    }
+
+    @Override
+    public int hashCode() {
+        int result = id;
+        result = 31 * result + (bookId != null ? bookId.hashCode() : 0);
+        result = 31 * result + (content != null ? content.hashCode() : 0);
+        result = 31 * result + (date != null ? date.hashCode() : 0);
+        result = 31 * result + (type != null ? type.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "HighlightImpl{" +
+                "id=" + id +
+                ", bookId='" + bookId + '\'' +
+                ", content='" + content + '\'' +
+                ", date=" + date +
+                ", type='" + type + '\'' +
+                ", pageNumber=" + pageNumber +
+                ", pageId='" + pageId + '\'' +
+                ", rangy='" + rangy + '\'' +
+                ", note='" + note + '\'' +
+                ", uuid='" + uuid + '\'' +
+                '}';
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(id);
+        dest.writeString(bookId);
+        dest.writeString(pageId);
+        dest.writeString(rangy);
+        dest.writeString(content);
+        dest.writeSerializable(date);
+        dest.writeString(type);
+        dest.writeInt(pageNumber);
+        dest.writeString(note);
+        dest.writeString(uuid);
+    }
+
+    private void readFromParcel(Parcel in) {
+        id = in.readInt();
+        bookId = in.readString();
+        pageId = in.readString();
+        rangy = in.readString();
+        content = in.readString();
+        date = (Date) in.readSerializable();
+        type = in.readString();
+        pageNumber = in.readInt();
+        note = in.readString();
+        uuid = in.readString();
+    }
+
+    public static final Creator<HighlightImpl> CREATOR = new Creator<HighlightImpl>() {
+        @Override
+        public HighlightImpl createFromParcel(Parcel in) {
+            return new HighlightImpl(in);
+        }
+
+        @Override
+        public HighlightImpl[] newArray(int size) {
+            return new HighlightImpl[size];
+        }
+    };
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/TOCLinkWrapper.java b/Android/folioreader/src/main/java/com/folioreader/model/TOCLinkWrapper.java
new file mode 100755 (executable)
index 0000000..6d2eac7
--- /dev/null
@@ -0,0 +1,91 @@
+package com.folioreader.model;
+
+import com.folioreader.util.MultiLevelExpIndListAdapter;
+
+import org.readium.r2_streamer.model.tableofcontents.TOCLink;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Mahavir on 3/10/17.
+ */
+
+public class TOCLinkWrapper implements MultiLevelExpIndListAdapter.ExpIndData{
+    private TOCLink tocLink;
+    private int indentation;
+    private ArrayList<TOCLinkWrapper> tocLinkWrappers;
+    private boolean mIsGroup;
+    private int mGroupSize;
+
+    public TOCLinkWrapper(TOCLink tocLink, int indentation) {
+        this.tocLink = tocLink;
+        this.indentation = indentation;
+        this.tocLinkWrappers = new ArrayList<>();
+        this.mIsGroup = (tocLink.getTocLinks()!=null && tocLink.getTocLinks().size()>0);
+    }
+
+    @Override
+    public String toString() {
+        return "TOCLinkWrapper{" +
+                "tocLink=" + tocLink +
+                ", indentation=" + indentation +
+                ", tocLinkWrappers=" + tocLinkWrappers +
+                ", mIsGroup=" + mIsGroup +
+                ", mGroupSize=" + mGroupSize +
+                '}';
+    }
+
+    public int getIndentation() {
+        return indentation;
+    }
+
+    public void setIndentation(int indentation) {
+        this.indentation = indentation;
+    }
+
+    public TOCLink getTocLink() {
+        return tocLink;
+    }
+
+    public void setTocLink(TOCLink tocLink) {
+        this.tocLink = tocLink;
+    }
+
+    public ArrayList<TOCLinkWrapper> getTocLinkWrappers() {
+        return tocLinkWrappers;
+    }
+
+    public void setTocLinkWrappers(ArrayList<TOCLinkWrapper> tocLinkWrappers) {
+        this.tocLinkWrappers = tocLinkWrappers;
+    }
+
+    public void addChild(TOCLinkWrapper tocLinkWrapper) {
+        getTocLinkWrappers().add(tocLinkWrapper);
+        //tocLinkWrapper.setIndentation(getIndentation() + 1);
+    }
+
+    @Override
+    public List<? extends MultiLevelExpIndListAdapter.ExpIndData> getChildren() {
+        return tocLinkWrappers;
+    }
+
+    @Override
+    public boolean isGroup() {
+        return mIsGroup;
+    }
+
+    @Override
+    public void setIsGroup(boolean value) {
+        mIsGroup = value;
+    }
+
+    @Override
+    public void setGroupSize(int groupSize) {
+        mGroupSize = groupSize;
+    }
+
+    public int getGroupSize() {
+        return mGroupSize;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Audio.java b/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Audio.java
new file mode 100755 (executable)
index 0000000..e674cb5
--- /dev/null
@@ -0,0 +1,50 @@
+package com.folioreader.model.dictionary;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Audio {
+    @JsonProperty
+    private String lang;
+    @JsonProperty
+    private String type;
+    @JsonProperty
+    private String url;
+
+    @Override
+    public String toString() {
+        return "Audio{" +
+                "lang='" + lang + '\'' +
+                ", type='" + type + '\'' +
+                ", url='" + url + '\'' +
+                '}';
+    }
+
+    public String getLang() {
+        return lang;
+    }
+
+    public void setLang(String lang) {
+        this.lang = lang;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Dictionary.java b/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Dictionary.java
new file mode 100755 (executable)
index 0000000..9783801
--- /dev/null
@@ -0,0 +1,56 @@
+package com.folioreader.model.dictionary;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * class is object representation of JSON received from
+ * open source dictionary API "pearson"
+ * ref => http://developer.pearson.com/apis/dictionaries
+ *
+ * @author gautam chibde on 4/7/17.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Dictionary {
+    @JsonProperty
+    private int status;
+    @JsonProperty
+    private String url;
+    @JsonProperty
+    private List<DictionaryResults> results;
+
+    @Override
+    public String toString() {
+        return "Dictionary{" +
+                "status=" + status +
+                ", url='" + url + '\'' +
+                ", results=" + results +
+                '}';
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public void setStatus(int status) {
+        this.status = status;
+    }
+
+    public List<DictionaryResults> getResults() {
+        return results;
+    }
+
+    public void setResults(List<DictionaryResults> results) {
+        this.results = results;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/dictionary/DictionaryResults.java b/Android/folioreader/src/main/java/com/folioreader/model/dictionary/DictionaryResults.java
new file mode 100755 (executable)
index 0000000..9d8c322
--- /dev/null
@@ -0,0 +1,64 @@
+package com.folioreader.model.dictionary;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class DictionaryResults {
+    @JsonProperty
+    private String headword;
+    @JsonProperty
+    private String part_of_speech;
+    @JsonProperty
+    private List<Pronunciations> pronunciations;
+    @JsonProperty
+    private List<Senses> senses;
+
+
+    @Override
+    public String toString() {
+        return "DictionaryResults{" +
+                "headword='" + headword + '\'' +
+                ", part_of_speech='" + part_of_speech + '\'' +
+                ", pronunciations=" + pronunciations +
+                ", senses=" + senses +
+                '}';
+    }
+
+    public String getHeadword() {
+        return headword;
+    }
+
+    public void setHeadword(String headword) {
+        this.headword = headword;
+    }
+
+    public String getPartOfSpeech() {
+        return part_of_speech;
+    }
+
+    public void setPartOfSpeech(String part_of_speech) {
+        this.part_of_speech = part_of_speech;
+    }
+
+    public List<Pronunciations> getPronunciations() {
+        return pronunciations;
+    }
+
+    public void setPronunciations(List<Pronunciations> pronunciations) {
+        this.pronunciations = pronunciations;
+    }
+
+    public List<Senses> getSenses() {
+        return senses;
+    }
+
+    public void setSenses(List<Senses> senses) {
+        this.senses = senses;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Example.java b/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Example.java
new file mode 100755 (executable)
index 0000000..fa2b2fc
--- /dev/null
@@ -0,0 +1,28 @@
+package com.folioreader.model.dictionary;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Example {
+    @JsonProperty
+    private String text;
+
+    @Override
+    public String toString() {
+        return "Example{" +
+                "text='" + text + '\'' +
+                '}';
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public void setText(String text) {
+        this.text = text;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Pronunciations.java b/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Pronunciations.java
new file mode 100755 (executable)
index 0000000..145cf2c
--- /dev/null
@@ -0,0 +1,31 @@
+package com.folioreader.model.dictionary;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Pronunciations {
+
+    @JsonProperty
+    private List<Audio> audio;
+
+    @Override
+    public String toString() {
+        return "Pronunciations{" +
+                "audio=" + audio +
+                '}';
+    }
+
+    public List<Audio> getAudio() {
+        return audio;
+    }
+
+    public void setAudio(List<Audio> audio) {
+        this.audio = audio;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Senses.java b/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Senses.java
new file mode 100755 (executable)
index 0000000..dc07b71
--- /dev/null
@@ -0,0 +1,78 @@
+package com.folioreader.model.dictionary;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.ObjectCodec;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Senses {
+    @JsonProperty
+    @JsonDeserialize(using = DefinitionDeserializer.class)
+    private String[] definition;
+    @JsonProperty
+    private List<Example> examples;
+
+    @Override
+    public String toString() {
+        return "Senses{" +
+                "definition=" + Arrays.toString(definition) +
+                ", examples=" + examples +
+                '}';
+    }
+
+    public String[] getDefinition() {
+        return definition;
+    }
+
+    public void setDefinition(String[] definition) {
+        this.definition = definition;
+    }
+
+    public List<Example> getExamples() {
+        return examples;
+    }
+
+    public void setExamples(List<Example> examples) {
+        this.examples = examples;
+    }
+
+    public static class DefinitionDeserializer extends StdDeserializer<String[]> {
+
+        public DefinitionDeserializer() {
+            super(String[].class);
+        }
+
+        protected DefinitionDeserializer(Class<?> vc) {
+            super(vc);
+        }
+
+        @Override
+        public String[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+            JsonNode node = p.getCodec().readTree(p);
+            List<String> strings = new ArrayList<>();
+            ObjectCodec oc = p.getCodec();
+            if (node.isArray()) {
+                for (JsonNode n : node) {
+                    strings.add(oc.treeToValue(n, String.class));
+                }
+            } else {
+                strings.add(oc.treeToValue(node, String.class));
+            }
+            return strings.toArray(new String[0]);
+        }
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Wikipedia.java b/Android/folioreader/src/main/java/com/folioreader/model/dictionary/Wikipedia.java
new file mode 100755 (executable)
index 0000000..4d15add
--- /dev/null
@@ -0,0 +1,47 @@
+package com.folioreader.model.dictionary;
+
+/**
+ * Created by gautam on 7/7/17.
+ */
+
+public class Wikipedia {
+    private String word;
+    private String definition;
+    private String link;
+
+    public Wikipedia() {
+    }
+
+    @Override
+    public String toString() {
+        return "Wikipedia{" +
+                "word='" + word + '\'' +
+                ", definition='" + definition + '\'' +
+                ", link='" + link + '\'' +
+                '}';
+    }
+
+    public String getWord() {
+        return word;
+    }
+
+    public void setWord(String word) {
+        this.word = word;
+    }
+
+    public String getDefinition() {
+        return definition;
+    }
+
+    public void setDefinition(String definition) {
+        this.definition = definition;
+    }
+
+    public String getLink() {
+        return link;
+    }
+
+    public void setLink(String link) {
+        this.link = link;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/event/AnchorIdEvent.java b/Android/folioreader/src/main/java/com/folioreader/model/event/AnchorIdEvent.java
new file mode 100755 (executable)
index 0000000..278bcfe
--- /dev/null
@@ -0,0 +1,20 @@
+package com.folioreader.model.event;
+
+/**
+ * Created by Shrikant on 7/28/2017.
+ */
+
+public class AnchorIdEvent {
+    private String href;
+
+    public AnchorIdEvent() {
+    }
+
+    public AnchorIdEvent(String href) {
+        this.href = href;
+    }
+
+    public String getHref() {
+        return href;
+    }
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/event/BusOwner.java b/Android/folioreader/src/main/java/com/folioreader/model/event/BusOwner.java
new file mode 100755 (executable)
index 0000000..eaf9b98
--- /dev/null
@@ -0,0 +1,7 @@
+package com.folioreader.model.event;
+
+import com.squareup.otto.Bus;
+
+public interface BusOwner {
+       Bus getBus();
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/event/MediaOverlayHighlightStyleEvent.java b/Android/folioreader/src/main/java/com/folioreader/model/event/MediaOverlayHighlightStyleEvent.java
new file mode 100755 (executable)
index 0000000..6308ca9
--- /dev/null
@@ -0,0 +1,21 @@
+package com.folioreader.model.event;
+
+/**
+ * @author gautam chibde on 14/6/17.
+ */
+
+public class MediaOverlayHighlightStyleEvent {
+    public enum Style {
+        UNDERLINE, BACKGROUND, DEFAULT,
+    }
+
+    private Style style;
+
+    public MediaOverlayHighlightStyleEvent(Style style) {
+        this.style = style;
+    }
+
+    public Style getStyle() {
+        return style;
+    }
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/event/MediaOverlayPlayPauseEvent.java b/Android/folioreader/src/main/java/com/folioreader/model/event/MediaOverlayPlayPauseEvent.java
new file mode 100755 (executable)
index 0000000..75432dc
--- /dev/null
@@ -0,0 +1,29 @@
+package com.folioreader.model.event;
+
+/**
+ * @author gautam chibde on 13/6/17.
+ */
+
+public class MediaOverlayPlayPauseEvent {
+    private boolean play;
+    private String href;
+    private boolean stateChanged;
+
+    public MediaOverlayPlayPauseEvent(String href, boolean play, boolean stateChanged) {
+        this.href = href;
+        this.play = play;
+        this.stateChanged = stateChanged;
+    }
+
+    public String getHref() {
+        return href;
+    }
+
+    public boolean isPlay() {
+        return play;
+    }
+
+    public boolean isStateChanged() {
+        return stateChanged;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/event/MediaOverlaySpeedEvent.java b/Android/folioreader/src/main/java/com/folioreader/model/event/MediaOverlaySpeedEvent.java
new file mode 100755 (executable)
index 0000000..823922b
--- /dev/null
@@ -0,0 +1,22 @@
+package com.folioreader.model.event;
+
+/**
+ * @author gautam chibde on 14/6/17.
+ */
+
+public class MediaOverlaySpeedEvent {
+
+    public enum Speed {
+        HALF, ONE, ONE_HALF, TWO,
+    }
+
+    private Speed speed;
+
+    public MediaOverlaySpeedEvent(Speed speed) {
+        this.speed = speed;
+    }
+
+    public Speed getSpeed() {
+        return speed;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/event/ReloadDataEvent.java b/Android/folioreader/src/main/java/com/folioreader/model/event/ReloadDataEvent.java
new file mode 100755 (executable)
index 0000000..6aaac3c
--- /dev/null
@@ -0,0 +1,8 @@
+package com.folioreader.model.event;
+
+/**
+ * This event is used when web page in {@link com.folioreader.ui.folio.fragment.FolioPageFragment}
+ * is to reloaded.
+ */
+public class ReloadDataEvent {
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/event/RewindIndexEvent.java b/Android/folioreader/src/main/java/com/folioreader/model/event/RewindIndexEvent.java
new file mode 100755 (executable)
index 0000000..94ba371
--- /dev/null
@@ -0,0 +1,8 @@
+package com.folioreader.model.event;
+
+/**
+ * Created by PC on 12/14/2016.
+ */
+
+public class RewindIndexEvent {
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/event/WebViewPosition.java b/Android/folioreader/src/main/java/com/folioreader/model/event/WebViewPosition.java
new file mode 100755 (executable)
index 0000000..9289ebc
--- /dev/null
@@ -0,0 +1,31 @@
+package com.folioreader.model.event;
+
+/**
+ * Created by PC on 12/24/2016.
+ */
+
+public class WebViewPosition {
+    private String href;
+    private String highlightId;
+
+    public String getHref() {
+        return href;
+    }
+
+    public WebViewPosition(String href, String highlightId) {
+        this.href = href;
+        this.highlightId = highlightId;
+    }
+
+    public void setHref(String href) {
+        this.href = href;
+    }
+
+    public String getHighlightId() {
+        return highlightId;
+    }
+
+    public void setHighlightId(String highlightId) {
+        this.highlightId = highlightId;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/media_overlay/OverlayItems.java b/Android/folioreader/src/main/java/com/folioreader/model/media_overlay/OverlayItems.java
new file mode 100755 (executable)
index 0000000..95d554f
--- /dev/null
@@ -0,0 +1,106 @@
+package com.folioreader.model.media_overlay;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * @author gautam chibde on 13/6/17.
+ */
+
+public class OverlayItems implements Parcelable {
+    private String id;
+    private String tag;
+    private String classType;
+    private String spineHref;
+    private String text;
+
+    public OverlayItems() {
+    }
+
+    public OverlayItems(String id, String tag) {
+        this.id = id;
+        this.tag = tag;
+    }
+
+    public OverlayItems(String id, String tag, String spineHref) {
+        this.id = id;
+        this.tag = tag;
+        this.spineHref = spineHref;
+    }
+
+    public OverlayItems(String id, String tag, String spineHref, String text) {
+        this.id = id;
+        this.tag = tag;
+        this.spineHref = spineHref;
+        this.text = text;
+    }
+
+    protected OverlayItems(Parcel in) {
+        id = in.readString();
+        tag = in.readString();
+        classType = in.readString();
+        spineHref = in.readString();
+        text = in.readString();
+    }
+
+    public static final Creator<OverlayItems> CREATOR = new Creator<OverlayItems>() {
+        @Override
+        public OverlayItems createFromParcel(Parcel in) {
+            return new OverlayItems(in);
+        }
+
+        @Override
+        public OverlayItems[] newArray(int size) {
+            return new OverlayItems[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        return "OverlayItems{" +
+                "id='" + id + '\'' +
+                ", tag='" + tag + '\'' +
+                ", classType='" + classType + '\'' +
+                ", spineHref='" + spineHref + '\'' +
+                ", text='" + text + '\'' +
+                '}';
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public String getTag() {
+        return tag;
+    }
+
+    public String getSpineHref() {
+        return spineHref;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public void setText(String text) {
+        this.text = text;
+    }
+
+    public void setSpineHref(String spineHref) {
+        this.spineHref = spineHref;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(id);
+        dest.writeString(tag);
+        dest.writeString(classType);
+        dest.writeString(spineHref);
+        dest.writeString(text);
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/quickaction/ActionItem.java b/Android/folioreader/src/main/java/com/folioreader/model/quickaction/ActionItem.java
new file mode 100755 (executable)
index 0000000..e3af451
--- /dev/null
@@ -0,0 +1,174 @@
+package com.folioreader.model.quickaction;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+
+/**
+ * Action item, displayed as menu with mIcon and text.
+ *
+ * @author Lorensius. W. L. T <lorenz@londatiga.net>
+ *
+ *         Contributors:
+ *         - Kevin Peck <kevinwpeck@gmail.com>
+ */
+public class ActionItem {
+    private Drawable mIcon;
+    private Bitmap mThumb;
+    private String mTitle;
+    private int mActionId = -1;
+    private boolean mSelected;
+    private boolean mSticky;
+
+    /**
+     * Constructor
+     *
+     * @param mActionId Action id for case statements
+     * @param mTitle    Title
+     * @param mIcon     Icon to use
+     */
+    public ActionItem(int mActionId, String mTitle, Drawable mIcon) {
+        this.mTitle = mTitle;
+        this.mIcon = mIcon;
+        this.mActionId = mActionId;
+    }
+
+    /**
+     * Constructor
+     */
+    public ActionItem() {
+        this(-1, null, null);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param mActionId Action id of the item
+     * @param mTitle    Text to show for the item
+     */
+    public ActionItem(int mActionId, String mTitle) {
+        this(mActionId, mTitle, null);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param mIcon {@link Drawable} action mIcon
+     */
+    public ActionItem(Drawable mIcon) {
+        this(-1, null, mIcon);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param mActionId Action ID of item
+     * @param mIcon     {@link Drawable} action mIcon
+     */
+    public ActionItem(int mActionId, Drawable mIcon) {
+        this(mActionId, null, mIcon);
+    }
+
+    /**
+     * Set action mTitle
+     *
+     * @param mTitle action mTitle
+     */
+    public void setTitle(String mTitle) {
+        this.mTitle = mTitle;
+    }
+
+    /**
+     * Get action mTitle
+     *
+     * @return action mTitle
+     */
+    public String getTitle() {
+        return this.mTitle;
+    }
+
+    /**
+     * Set action mIcon
+     *
+     * @param mIcon {@link Drawable} action mIcon
+     */
+    public void setIcon(Drawable mIcon) {
+        this.mIcon = mIcon;
+    }
+
+    /**
+     * Get action mIcon
+     *
+     * @return {@link Drawable} action mIcon
+     */
+    public Drawable getIcon() {
+        return this.mIcon;
+    }
+
+    /**
+     * Set action id
+     *
+     * @param mActionId Action id for this action
+     */
+    public void setActionId(int mActionId) {
+        this.mActionId = mActionId;
+    }
+
+    /**
+     * @return Our action id
+     */
+    public int getActionId() {
+        return mActionId;
+    }
+
+    /**
+     * Set mSticky status of button
+     *
+     * @param mSticky true for mSticky, pop up sends event but does not disappear
+     */
+    public void setSticky(boolean mSticky) {
+        this.mSticky = mSticky;
+    }
+
+    /**
+     * @return true if button is mSticky, menu stays visible after press
+     */
+    public boolean isSticky() {
+        return mSticky;
+    }
+
+    /**
+     * Set mSelected flag;
+     *
+     * @param mSelected Flag to indicate the item is mSelected
+     */
+    public void setSelected(boolean mSelected) {
+        this.mSelected = mSelected;
+    }
+
+    /**
+     * Check if item is mSelected
+     *
+     * @return true or false
+     */
+    public boolean isSelected() {
+        return this.mSelected;
+    }
+
+    /**
+     * Set mThumb
+     *
+     * @param mThumb Thumb image
+     */
+    public void setThumb(Bitmap mThumb) {
+        this.mThumb = mThumb;
+    }
+
+    /**
+     * Get mThumb image
+     *
+     * @return Thumb image
+     */
+    public Bitmap getThumb() {
+        return this.mThumb;
+    }
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/quickaction/PopupWindows.java b/Android/folioreader/src/main/java/com/folioreader/model/quickaction/PopupWindows.java
new file mode 100755 (executable)
index 0000000..25db41a
--- /dev/null
@@ -0,0 +1,131 @@
+package com.folioreader.model.quickaction;\r
+\r
+import android.content.Context;\r
+import android.graphics.drawable.BitmapDrawable;\r
+import android.graphics.drawable.Drawable;\r
+import android.view.LayoutInflater;\r
+import android.view.MotionEvent;\r
+import android.view.View;\r
+import android.view.View.OnTouchListener;\r
+import android.view.WindowManager;\r
+import android.widget.PopupWindow;\r
+\r
+/**\r
+ * Custom popup window.\r
+ *\r
+ * @author Lorensius W. L. T <lorenz@londatiga.net>\r
+ */\r
+public class PopupWindows {\r
+    protected Context mContext;\r
+    protected PopupWindow mWindow;\r
+    protected View mRootView;\r
+    protected Drawable mBackground = null;\r
+    protected WindowManager mWindowManager;\r
+\r
+    /**\r
+     * Constructor.\r
+     *\r
+     * @param context Context\r
+     */\r
+    public PopupWindows(Context context) {\r
+        mContext = context;\r
+        mWindow = new PopupWindow(context);\r
+\r
+        mWindow.setTouchInterceptor(new OnTouchListener() {\r
+            @Override\r
+            public boolean onTouch(View v, MotionEvent event) {\r
+                if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {\r
+                    mWindow.dismiss();\r
+\r
+                    return true;\r
+                }\r
+\r
+                return false;\r
+            }\r
+        });\r
+\r
+        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\r
+    }\r
+\r
+    /**\r
+     * On dismiss\r
+     */\r
+    protected void onDismiss() {\r
+    }\r
+\r
+    /**\r
+     * On show\r
+     */\r
+    protected void onShow() {\r
+    }\r
+\r
+    /**\r
+     * On pre show\r
+     */\r
+    protected void preShow() {\r
+        if (mRootView == null)\r
+            throw new IllegalStateException("setContentView was not called with a view to display");\r
+\r
+        onShow();\r
+\r
+        if (mBackground == null) {\r
+            mWindow.setBackgroundDrawable(new BitmapDrawable());\r
+        } else {\r
+            mWindow.setBackgroundDrawable(mBackground);\r
+        }\r
+\r
+        mWindow.setWidth(WindowManager.LayoutParams.WRAP_CONTENT);\r
+        mWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);\r
+        mWindow.setTouchable(true);\r
+        mWindow.setFocusable(true);\r
+        mWindow.setOutsideTouchable(true);\r
+\r
+        mWindow.setContentView(mRootView);\r
+    }\r
+\r
+    /**\r
+     * Set background drawable.\r
+     *\r
+     * @param background Background drawable\r
+     */\r
+    public void setBackgroundDrawable(Drawable background) {\r
+        mBackground = background;\r
+    }\r
+\r
+    /**\r
+     * Set content view.\r
+     *\r
+     * @param root Root view\r
+     */\r
+    public void setContentView(View root) {\r
+        mRootView = root;\r
+\r
+        mWindow.setContentView(root);\r
+    }\r
+\r
+    /**\r
+     * Set content view.\r
+     *\r
+     * @param layoutResID Resource id\r
+     */\r
+    public void setContentView(int layoutResID) {\r
+        LayoutInflater inflator =\r
+                (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);\r
+\r
+        setContentView(inflator.inflate(layoutResID, null));\r
+    }\r
+\r
+    /**\r
+     * Set listener on window dismissed.\r
+     */\r
+    public void setOnDismissListener(PopupWindow.OnDismissListener listener) {\r
+        mWindow.setOnDismissListener(listener);\r
+    }\r
+\r
+    /**\r
+     * Dismiss the popup window.\r
+     */\r
+    public void dismiss() {\r
+        mWindow.dismiss();\r
+    }\r
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/quickaction/QuickAction.java b/Android/folioreader/src/main/java/com/folioreader/model/quickaction/QuickAction.java
new file mode 100755 (executable)
index 0000000..c9368a6
--- /dev/null
@@ -0,0 +1,475 @@
+package com.folioreader.model.quickaction;
+
+import com.folioreader.R;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ImageView;
+import android.widget.PopupWindow.OnDismissListener;
+import android.widget.RelativeLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * QuickAction dialog, shows action list as icon and text like the one in Gallery3D app. Currently
+ * supports vertical
+ * and horizontal layout.
+ *
+ * @author Lorensius W. L. T <lorenz@londatiga.net>
+ *
+ *         Contributors:
+ *         - Kevin Peck <kevinwpeck@gmail.com>
+ */
+public class QuickAction extends PopupWindows implements OnDismissListener {
+    private View mRootView;
+    private ImageView mArrowUp;
+    private ImageView mArrowDown;
+    private LayoutInflater mInflater;
+    private ViewGroup mTrack;
+    private ScrollView mScroller;
+    private OnActionItemClickListener mItemClickListener;
+    private OnDismissListener mDismissListener;
+
+    private List<ActionItem> mActionItems = new ArrayList<ActionItem>();
+
+    private boolean mDidAction;
+
+    private int mChildPos;
+    private int mInsertPos;
+    private int mAnimStyle;
+    private int mOrientation;
+    private int mRootWidth = 0;
+
+    public static final int HORIZONTAL = 0;
+    public static final int VERTICAL = 1;
+
+    public static final int ANIM_GROW_FROM_LEFT = 1;
+    public static final int ANIM_GROW_FROM_RIGHT = 2;
+    public static final int ANIM_GROW_FROM_CENTER = 3;
+    public static final int ANIM_REFLECT = 4;
+    public static final int ANIM_AUTO = 5;
+
+    /**
+     * Constructor for default vertical layout
+     *
+     * @param context Context
+     */
+    public QuickAction(Context context) {
+        this(context, VERTICAL);
+    }
+
+    /**
+     * Constructor allowing orientation override
+     *
+     * @param context     Context
+     * @param orientation Layout orientation, can be vartical or horizontal
+     */
+    public QuickAction(Context context, int orientation) {
+        super(context);
+
+        mOrientation = orientation;
+
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        if (mOrientation == HORIZONTAL) {
+            setRootViewId(R.layout.popup_horizontal);
+        } else {
+            setRootViewId(R.layout.popup_vertical);
+        }
+
+        mAnimStyle = ANIM_AUTO;
+        mChildPos = 0;
+    }
+
+    /**
+     * Get action item at an index
+     *
+     * @param index Index of item (position from callback)
+     * @return Action Item at the position
+     */
+    public ActionItem getActionItem(int index) {
+        return mActionItems.get(index);
+    }
+
+    /**
+     * Set root view.
+     *
+     * @param id Layout resource id
+     */
+    public void setRootViewId(int id) {
+        mRootView = (ViewGroup) mInflater.inflate(id, null);
+        mTrack = (ViewGroup) mRootView.findViewById(R.id.tracks);
+
+        mArrowDown = (ImageView) mRootView.findViewById(R.id.arrow_down);
+        mArrowUp = (ImageView) mRootView.findViewById(R.id.arrow_up);
+
+        mScroller = (ScrollView) mRootView.findViewById(R.id.scroller);
+
+        //This was previously defined on show()
+        // method, moved here to prevent force close that occured
+        //when tapping fastly on a view to show quickaction dialog.
+        //Thanx to zammbi (github.com/zammbi)
+        mRootView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT));
+
+        setContentView(mRootView);
+    }
+
+    /**
+     * Set animation style
+     *
+     * @param mAnimStyle animation style, default is set to ANIM_AUTO
+     */
+    public void setAnimStyle(int mAnimStyle) {
+        this.mAnimStyle = mAnimStyle;
+    }
+
+    /**
+     * Set listener for action item clicked.
+     *
+     * @param listener Listener
+     */
+    public void setOnActionItemClickListener(OnActionItemClickListener listener) {
+        mItemClickListener = listener;
+    }
+
+    /**
+     * Add action item
+     *
+     * @param action {@link ActionItem}
+     */
+    public void addActionItem(ActionItem action) {
+        mActionItems.add(action);
+
+        String title = action.getTitle();
+        Drawable icon = action.getIcon();
+
+        View container;
+
+        if (mOrientation == HORIZONTAL) {
+            container = mInflater.inflate(R.layout.action_item_horizontal, null);
+        } else {
+            container = mInflater.inflate(R.layout.action_item_vertical, null);
+        }
+
+        ImageView img = (ImageView) container.findViewById(R.id.iv_icon);
+        TextView text = (TextView) container.findViewById(R.id.tv_title);
+
+        if (icon != null) {
+            img.setImageDrawable(icon);
+        } else {
+            img.setVisibility(View.GONE);
+        }
+
+        if (title != null) {
+            text.setText(title);
+        } else {
+            text.setVisibility(View.GONE);
+        }
+
+        final int pos = mChildPos;
+        final int actionId = action.getActionId();
+
+        container.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mItemClickListener != null) {
+                    mItemClickListener.onItemClick(QuickAction.this, pos, actionId);
+                }
+
+                if (!getActionItem(pos).isSticky()) {
+                    mDidAction = true;
+
+                    dismiss();
+                }
+            }
+        });
+
+        container.setFocusable(true);
+        container.setClickable(true);
+
+        if (mOrientation == HORIZONTAL && mChildPos != 0) {
+            View separator = mInflater.inflate(R.layout.horiz_separator, null);
+
+            RelativeLayout.LayoutParams params =
+                    new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+                            LayoutParams.FILL_PARENT);
+
+            separator.setLayoutParams(params);
+            separator.setPadding(5, 0, 5, 0);
+
+            mTrack.addView(separator, mInsertPos);
+
+            mInsertPos++;
+        }
+
+        mTrack.addView(container, mInsertPos);
+
+        mChildPos++;
+        mInsertPos++;
+    }
+
+    /**
+     * Show quickaction popup. Popup is automatically positioned, on top or bottom of anchor view.
+     */
+    public void show(View anchor) {
+        preShow();
+
+        int xPos, yPos, arrowPos;
+
+        mDidAction = false;
+
+        int[] location = new int[2];
+
+        anchor.getLocationOnScreen(location);
+
+        Rect anchorRect = new Rect(location[0], location[1], location[0]
+                + anchor.getWidth(), location[1]
+                + anchor.getHeight());
+
+        mRootView.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+
+        int rootHeight = mRootView.getMeasuredHeight();
+
+        if (mRootWidth == 0) {
+            mRootWidth = mRootView.getMeasuredWidth();
+        }
+
+        int screenWidth = mWindowManager.getDefaultDisplay().getWidth();
+        int screenHeight = mWindowManager.getDefaultDisplay().getHeight();
+
+        //automatically get X coord of popup (top left)
+        if ((anchorRect.left + mRootWidth) > screenWidth) {
+            xPos = anchorRect.left - (mRootWidth - anchor.getWidth());
+            xPos = (xPos < 0) ? 0 : xPos;
+
+            arrowPos = anchorRect.centerX() - xPos;
+
+        } else {
+            if (anchor.getWidth() > mRootWidth) {
+                xPos = anchorRect.centerX() - (mRootWidth / 2);
+            } else {
+                xPos = anchorRect.left;
+            }
+
+            arrowPos = anchorRect.centerX() - xPos;
+        }
+
+        int dyTop = anchorRect.top;
+        int dyBottom = screenHeight - anchorRect.bottom;
+
+        boolean onTop = (dyTop > dyBottom) ? true : false;
+
+        if (onTop) {
+            if (rootHeight > dyTop) {
+                yPos = 15;
+                LayoutParams l = mScroller.getLayoutParams();
+                l.height = dyTop - anchor.getHeight();
+            } else {
+                yPos = anchorRect.top - rootHeight;
+            }
+        } else {
+            yPos = anchorRect.bottom;
+
+            if (rootHeight > dyBottom) {
+                LayoutParams l = mScroller.getLayoutParams();
+                l.height = dyBottom;
+            }
+        }
+
+        showArrow(((onTop) ? R.id.arrow_down : R.id.arrow_up), arrowPos);
+
+        setAnimationStyle(screenWidth, anchorRect.centerX(), onTop);
+
+        mWindow.showAtLocation(anchor, Gravity.NO_GRAVITY, xPos, yPos);
+    }
+
+    public void show(View anchor, int width, int height) {
+        preShow();
+
+        int xPos, yPos, arrowPos;
+        int[] location = new int[2];
+
+        anchor.getLocationOnScreen(location);
+
+        Rect anchorRect =
+                new Rect(location[0], location[1], location[0] + width, location[1] + height);
+
+        mDidAction = false;
+
+        mRootView.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+
+        int rootHeight = mRootView.getMeasuredHeight();
+
+        if (mRootWidth == 0) {
+            mRootWidth = mRootView.getMeasuredWidth();
+        }
+
+        int screenWidth = mWindowManager.getDefaultDisplay().getWidth();
+        int screenHeight = mWindowManager.getDefaultDisplay().getHeight();
+
+        //automatically get X coord of popup (top left)
+        if ((anchorRect.left + mRootWidth) > screenWidth) {
+            xPos = anchorRect.left - (mRootWidth - width);
+            xPos = (xPos < 0) ? 0 : xPos;
+
+            arrowPos = anchorRect.centerX() - xPos;
+
+        } else {
+            if (width > mRootWidth) {
+                xPos = anchorRect.centerX() - (mRootWidth / 2);
+            } else {
+                xPos = anchorRect.left;
+            }
+
+            arrowPos = anchorRect.centerX() - xPos;
+        }
+
+        int dyTop = anchorRect.top;
+        int dyBottom = screenHeight - anchorRect.bottom;
+
+        boolean onTop = (dyTop > dyBottom) ? true : false;
+
+        if (onTop) {
+            if (rootHeight > dyTop) {
+                yPos = 15;
+                LayoutParams l = mScroller.getLayoutParams();
+                l.height = dyTop - height;
+            } else {
+                yPos = anchorRect.top - rootHeight;
+            }
+        } else {
+            yPos = anchorRect.bottom;
+
+            if (rootHeight > dyBottom) {
+                LayoutParams l = mScroller.getLayoutParams();
+                l.height = dyBottom;
+            }
+        }
+
+        showArrow(((onTop) ? R.id.arrow_down : R.id.arrow_up), arrowPos);
+
+        setAnimationStyle(screenWidth, anchorRect.centerX(), onTop);
+
+        mWindow.showAtLocation(anchor, Gravity.NO_GRAVITY, xPos, yPos);
+    }
+
+    /**
+     * Set animation style
+     *
+     * @param screenWidth screen width
+     * @param requestedX  distance from left edge
+     * @param onTop       flag to indicate where the popup should be displayed. Set TRUE if
+     *                    displayed on top of anchor view
+     *                    and vice versa
+     */
+    private void setAnimationStyle(int screenWidth, int requestedX, boolean onTop) {
+        int arrowPos = requestedX - mArrowUp.getMeasuredWidth() / 2;
+
+        switch (mAnimStyle) {
+            case ANIM_GROW_FROM_LEFT:
+                mWindow.setAnimationStyle((onTop) ?
+                        R.style.Animations_PopUpMenu_Left : R.style.Animations_PopDownMenu_Left);
+                break;
+
+            case ANIM_GROW_FROM_RIGHT:
+                mWindow.setAnimationStyle((onTop) ?
+                        R.style.Animations_PopUpMenu_Right : R.style.Animations_PopDownMenu_Right);
+                break;
+
+            case ANIM_GROW_FROM_CENTER:
+                mWindow.setAnimationStyle((onTop) ?
+                        R.style.Animations_PopUpMenu_Center :
+                        R.style.Animations_PopDownMenu_Center);
+                break;
+
+            case ANIM_REFLECT:
+                mWindow.setAnimationStyle((onTop) ?
+                        R.style.Animations_PopUpMenu_Reflect :
+                        R.style.Animations_PopDownMenu_Reflect);
+                break;
+
+            case ANIM_AUTO:
+                if (arrowPos <= screenWidth / 4) {
+                    mWindow.setAnimationStyle((onTop) ?
+                            R.style.Animations_PopUpMenu_Left :
+                            R.style.Animations_PopDownMenu_Left);
+                } else if (arrowPos > screenWidth / 4 &&
+                        arrowPos < 3 * (screenWidth / 4)) {
+                    mWindow.setAnimationStyle((onTop) ?
+                            R.style.Animations_PopUpMenu_Center :
+                            R.style.Animations_PopDownMenu_Center);
+                } else {
+                    mWindow.setAnimationStyle((onTop) ?
+                            R.style.Animations_PopUpMenu_Right :
+                            R.style.Animations_PopDownMenu_Right);
+                }
+
+                break;
+        }
+    }
+
+    /**
+     * Show arrow
+     *
+     * @param whichArrow arrow type resource id
+     * @param requestedX distance from left screen
+     */
+    private void showArrow(int whichArrow, int requestedX) {
+        final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
+        final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
+
+        final int arrowWidth = mArrowUp.getMeasuredWidth();
+
+        showArrow.setVisibility(View.VISIBLE);
+
+        ViewGroup.MarginLayoutParams param =
+                (ViewGroup.MarginLayoutParams) showArrow.getLayoutParams();
+
+        param.leftMargin = requestedX - arrowWidth / 2;
+
+        hideArrow.setVisibility(View.INVISIBLE);
+    }
+
+    /**
+     * Set listener for window dismissed. This listener will only be fired if the quicakction dialog
+     * is dismissed
+     * by clicking outside the dialog or clicking on sticky item.
+     */
+    public void setOnDismissListener(QuickAction.OnDismissListener listener) {
+        setOnDismissListener(this);
+
+        mDismissListener = listener;
+    }
+
+    @Override
+    public void onDismiss() {
+        if (!mDidAction && mDismissListener != null) {
+            mDismissListener.onDismiss();
+        }
+    }
+
+    /**
+     * Listener for item click
+     */
+    public interface OnActionItemClickListener {
+        public abstract void onItemClick(QuickAction source, int pos, int actionId);
+    }
+
+    /**
+     * Listener for window dismiss
+     */
+    public interface OnDismissListener {
+        public abstract void onDismiss();
+    }
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/sqlite/DbAdapter.java b/Android/folioreader/src/main/java/com/folioreader/model/sqlite/DbAdapter.java
new file mode 100755 (executable)
index 0000000..9d0b2a6
--- /dev/null
@@ -0,0 +1,99 @@
+package com.folioreader.model.sqlite;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+
+public class DbAdapter {
+    private static final String TAG = "DBAdapter";
+
+    private Context mContext;
+    public static SQLiteDatabase mDatabase;
+
+    public DbAdapter(Context ctx) {
+        this.mContext = ctx;
+        mDatabase = FolioDatabaseHelper.getInstance(mContext).getMyWritableDatabase();
+    }
+
+    public static boolean insert(String table, ContentValues contentValues) {
+
+        return mDatabase.insert(table, null, contentValues) > 0;
+    }
+
+    public static boolean update(String table, String key, String value, ContentValues contentValues) {
+
+        return mDatabase.update(table, contentValues, key + "=?", new String[]{value}) > 0;
+    }
+
+    public static Cursor getHighLightsForBookId(String bookId) {
+        return mDatabase.rawQuery("SELECT * FROM " + HighLightTable.TABLE_NAME + " WHERE " + HighLightTable.COL_BOOK_ID + " = '" + bookId + "'", null);
+    }
+
+    public boolean deleteAll(String table) {
+        return mDatabase.delete(table, null, null) > 0;
+    }
+
+    public boolean deleteAll(String table, String whereClause, String[] whereArgs) {
+        return mDatabase.delete(table, whereClause + "=?", whereArgs) > 0;
+    }
+
+    public Cursor getAll(String table, String[] projection, String selection,
+                         String[] selectionArgs, String orderBy) {
+        return mDatabase.query(table, projection, selection, selectionArgs, null, null, orderBy);
+    }
+
+    public Cursor getAll(String table) {
+        return getAll(table, null, null, null, null);
+    }
+
+    public Cursor get(String table, long id, String[] projection, String key) throws SQLException {
+        return mDatabase.query(table, projection,
+                key + "=?", new String[]{String.valueOf(id)}, null, null, null, null);
+    }
+
+    public static Cursor getAllByKey(String table, String[] projection, String key, String value) throws SQLException {
+        return mDatabase.query(table, projection,
+                key + "=?", new String[]{value}, null, null, null, null);
+    }
+
+    public Cursor get(String table, long id) throws SQLException {
+        return get(table, id, null, FolioDatabaseHelper.KEY_ID);
+    }
+
+    public static boolean deleteById(String table, String key, String value) {
+        return mDatabase.delete(table, key + "=?", new String[]{value}) > 0;
+    }
+
+    public Cursor getMaxId(String tableName, String key) {
+        return mDatabase.rawQuery("SELECT MAX(" + key + ") FROM " + tableName, null);
+    }
+
+    public static long saveHighLight(ContentValues highlightContentValues) {
+        return mDatabase.insert(HighLightTable.TABLE_NAME, null, highlightContentValues);
+    }
+
+    public static boolean updateHighLight(ContentValues highlightContentValues, String id) {
+        return mDatabase.update(HighLightTable.TABLE_NAME, highlightContentValues, HighLightTable.ID + " = " + id, null) > 0;
+    }
+
+    public static Cursor getHighlightsForPageId(String query, String pageId) {
+        return mDatabase.rawQuery(query, null);
+    }
+
+    public static int getIdForQuery(String query) {
+        Cursor c = mDatabase.rawQuery(query, null);
+
+        int id = -1;
+        while (c.moveToNext()) {
+            id = c.getInt(c.getColumnIndex(HighLightTable.ID));
+        }
+        c.close();
+        return id;
+    }
+
+    public static Cursor getHighlightsForId(int id) {
+        return mDatabase.rawQuery("SELECT * FROM " + HighLightTable.TABLE_NAME + " WHERE " + HighLightTable.ID + " = '" + id + "'", null);
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/sqlite/DictionaryTable.java b/Android/folioreader/src/main/java/com/folioreader/model/sqlite/DictionaryTable.java
new file mode 100755 (executable)
index 0000000..952c34d
--- /dev/null
@@ -0,0 +1,87 @@
+package com.folioreader.model.sqlite;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author gautam chibde on 28/6/17.
+ */
+
+public class DictionaryTable {
+
+    public static final String TABLE_NAME = "dictionary_table";
+
+    public static final String ID = "_id";
+    public static final String WORD = "word";
+    public static final String MEANING = "meaning";
+    private SQLiteDatabase database;
+
+    public DictionaryTable(Context context) {
+        FolioDatabaseHelper dbHelper = new FolioDatabaseHelper(context);
+        database = dbHelper.getWritableDatabase();
+    }
+
+    public static final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ( " + ID
+            + " INTEGER PRIMARY KEY AUTOINCREMENT" + ","
+            + WORD + " TEXT" + ","
+            + MEANING + " TEXT" + ")";
+
+    public static final String SQL_DROP = "DROP TABLE IF EXISTS " + TABLE_NAME;
+
+    public boolean insertWord(String word, String meaning) {
+        ContentValues values = new ContentValues();
+        values.put(WORD, word);
+        values.put(MEANING, meaning);
+        return database.insert(TABLE_NAME, null, values) > 0;
+    }
+
+    public void insert(Map<String, String> map) {
+        database.beginTransaction();
+        for (String key : map.keySet()) {
+            insertWord(key.toLowerCase(), map.get(key));
+        }
+        database.setTransactionSuccessful();
+        database.endTransaction();
+    }
+
+    public String getMeaningForWord(String word) {
+        Cursor c = database.rawQuery("SELECT * FROM "
+                + TABLE_NAME +
+                " WHERE " + WORD + " = '" + word.trim() + "'", null);
+        if (c.moveToFirst()) {
+            String toRetuen = c.getString(2);
+            c.close();
+            return toRetuen;
+        }
+        c.close();
+        return null;
+    }
+
+    public List<String> getMeaning(String word) {
+        List<String> words = new ArrayList<>();
+        String meaning = getMeaningForWord(word);
+        if (meaning != null) {
+            words.add(meaning);
+            return words;
+        } else {
+            return getProbableCombinations(word);
+        }
+    }
+
+    private List<String> getProbableCombinations(String word) {
+        List<String> combinations = new ArrayList<>();
+        for (int i = 1; i <= 3; i++) {
+            String m = getMeaningForWord(word.substring(0, word.length() - i));
+            if (m != null) {
+                combinations.add(m);
+            }
+        }
+        return combinations;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/sqlite/FolioDatabaseHelper.java b/Android/folioreader/src/main/java/com/folioreader/model/sqlite/FolioDatabaseHelper.java
new file mode 100755 (executable)
index 0000000..188898e
--- /dev/null
@@ -0,0 +1,85 @@
+package com.folioreader.model.sqlite;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+public class FolioDatabaseHelper extends SQLiteOpenHelper {
+    @SuppressWarnings("unused")
+    private static final String TAG = "SQLiteOpenHelper";
+
+    private static FolioDatabaseHelper mInstance;
+    private static SQLiteDatabase myWritableDb;
+
+    public static final String DATABASE_NAME = "FolioReader.db";
+    private static final int DATABASE_VERSION = 2;
+
+    public static final String KEY_ID = "_id";
+    private final Context mContext;
+
+    public FolioDatabaseHelper(final Context context) {
+        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+        mContext = context;
+    }
+
+    public static FolioDatabaseHelper getInstance(Context context) {
+        if (mInstance == null) {
+            mInstance = new FolioDatabaseHelper(context);
+        }
+        return mInstance;
+    }
+
+    public SQLiteDatabase getMyWritableDatabase() {
+        if ((myWritableDb == null) || (!myWritableDb.isOpen())) {
+            myWritableDb = this.getWritableDatabase();
+        }
+
+        return myWritableDb;
+    }
+
+    @Override
+    public void close() {
+        super.close();
+        if (myWritableDb != null) {
+            myWritableDb.close();
+            myWritableDb = null;
+        }
+    }
+
+    @Override
+    public final void onCreate(final SQLiteDatabase db) {
+        Log.d("create table highlight", "****" + HighLightTable.SQL_CREATE);
+        db.execSQL(HighLightTable.SQL_CREATE);
+    }
+
+    @Override
+    public final void onUpgrade(final SQLiteDatabase db, final int oldVersion,
+                                final int newVersion) {
+               /* PROTECTED REGION ID(DatabaseUpdate) ENABLED START */
+
+        // TODO Implement your database update functionality here and remove the
+        // following method call!
+        //onUpgradeDropTables(db);
+        //onCreate(db);
+        resetAllPreferences(mContext);
+
+               /* PROTECTED REGION END */
+    }
+
+    /**
+     * This basic upgrade functionality will destroy all old data on upgrade
+     */
+    private final void onUpgradeDropTables(final SQLiteDatabase db) {
+
+    }
+
+    /**
+     * Resets all shared preferences
+     *
+     * @param context
+     */
+    private final void resetAllPreferences(Context context) {
+
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/model/sqlite/HighLightTable.java b/Android/folioreader/src/main/java/com/folioreader/model/sqlite/HighLightTable.java
new file mode 100755 (executable)
index 0000000..7f6a2d2
--- /dev/null
@@ -0,0 +1,200 @@
+package com.folioreader.model.sqlite;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.folioreader.Constants;
+import com.folioreader.model.HighLight;
+import com.folioreader.model.HighlightImpl;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+@SuppressWarnings("PMD.AvoidDuplicateLiterals")
+public class HighLightTable {
+    public static final String TABLE_NAME = "highlight_table";
+
+    public static final String ID = "_id";
+    public static final String COL_BOOK_ID = "bookId";
+    private static final String COL_CONTENT = "content";
+    private static final String COL_DATE = "date";
+    private static final String COL_TYPE = "type";
+    private static final String COL_PAGE_NUMBER = "page_number";
+    private static final String COL_PAGE_ID = "pageId";
+    private static final String COL_RANGY = "rangy";
+    private static final String COL_NOTE = "note";
+    private static final String COL_UUID = "uuid";
+
+    public static final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ( " + ID
+            + " INTEGER PRIMARY KEY AUTOINCREMENT" + ","
+            + COL_BOOK_ID + " TEXT" + ","
+            + COL_CONTENT + " TEXT" + ","
+            + COL_DATE + " TEXT" + ","
+            + COL_TYPE + " TEXT" + ","
+            + COL_PAGE_NUMBER + " INTEGER" + ","
+            + COL_PAGE_ID + " TEXT" + ","
+            + COL_RANGY + " TEXT" + ","
+            + COL_UUID + " TEXT" + ","
+            + COL_NOTE + " TEXT" + ")";
+
+    public static final String SQL_DROP = "DROP TABLE IF EXISTS " + TABLE_NAME;
+
+    public static final String TAG = HighLightTable.class.getSimpleName();
+
+    public static ContentValues getHighlightContentValues(HighLight highLight) {
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(COL_BOOK_ID, highLight.getBookId());
+        contentValues.put(COL_CONTENT, highLight.getContent());
+        contentValues.put(COL_DATE, getDateTimeString(highLight.getDate()));
+        contentValues.put(COL_TYPE, highLight.getType());
+        contentValues.put(COL_PAGE_NUMBER, highLight.getPageNumber());
+        contentValues.put(COL_PAGE_ID, highLight.getPageId());
+        contentValues.put(COL_RANGY, highLight.getRangy());
+        contentValues.put(COL_NOTE, highLight.getNote());
+        contentValues.put(COL_UUID, highLight.getUUID());
+        return contentValues;
+    }
+
+
+    public static ArrayList<HighlightImpl> getAllHighlights(String bookId) {
+        ArrayList<HighlightImpl> highlights = new ArrayList<>();
+        Cursor highlightCursor = DbAdapter.getHighLightsForBookId(bookId);
+        while (highlightCursor.moveToNext()) {
+            highlights.add(new HighlightImpl(highlightCursor.getInt(highlightCursor.getColumnIndex(ID)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_BOOK_ID)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_CONTENT)),
+                    getDateTime(highlightCursor.getString(highlightCursor.getColumnIndex(COL_DATE))),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_TYPE)),
+                    highlightCursor.getInt(highlightCursor.getColumnIndex(COL_PAGE_NUMBER)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_PAGE_ID)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_RANGY)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_NOTE)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_UUID))));
+        }
+        return highlights;
+    }
+
+    public static HighlightImpl getHighlightId(int id) {
+        Cursor highlightCursor = DbAdapter.getHighlightsForId(id);
+        HighlightImpl highlightImpl = new HighlightImpl();
+        while (highlightCursor.moveToNext()) {
+            highlightImpl = new HighlightImpl(highlightCursor.getInt(highlightCursor.getColumnIndex(ID)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_BOOK_ID)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_CONTENT)),
+                    getDateTime(highlightCursor.getString(highlightCursor.getColumnIndex(COL_DATE))),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_TYPE)),
+                    highlightCursor.getInt(highlightCursor.getColumnIndex(COL_PAGE_NUMBER)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_PAGE_ID)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_RANGY)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_NOTE)),
+                    highlightCursor.getString(highlightCursor.getColumnIndex(COL_UUID)));
+
+        }
+        return highlightImpl;
+    }
+
+    public static long insertHighlight(HighlightImpl highlightImpl) {
+        highlightImpl.setUUID(UUID.randomUUID().toString());
+        return DbAdapter.saveHighLight(getHighlightContentValues(highlightImpl));
+    }
+
+    public static boolean deleteHighlight(String rangy) {
+        String query = "SELECT " + ID + " FROM " + TABLE_NAME + " WHERE " + COL_RANGY + " = '" + rangy + "'";
+        int id = DbAdapter.getIdForQuery(query);
+        return id != -1 && deleteHighlight(id);
+    }
+
+    public static boolean deleteHighlight(int highlightId) {
+        return DbAdapter.deleteById(TABLE_NAME, ID, String.valueOf(highlightId));
+    }
+
+    public static List<String> getHighlightsForPageId(String pageId) {
+        String query = "SELECT " + COL_RANGY + " FROM " + TABLE_NAME + " WHERE " + COL_PAGE_ID + " = '" + pageId + "'";
+        Cursor c = DbAdapter.getHighlightsForPageId(query, pageId);
+        List<String> rangyList = new ArrayList<>();
+        while (c.moveToNext()) {
+            rangyList.add(c.getString(c.getColumnIndex(COL_RANGY)));
+        }
+        c.close();
+        return rangyList;
+    }
+
+    public static boolean updateHighlight(HighlightImpl highlightImpl) {
+        return DbAdapter.updateHighLight(getHighlightContentValues(highlightImpl), String.valueOf(highlightImpl.getId()));
+    }
+
+    public static String getDateTimeString(Date date) {
+        SimpleDateFormat dateFormat = new SimpleDateFormat(
+                Constants.DATE_FORMAT, Locale.getDefault());
+        return dateFormat.format(date);
+    }
+
+    public static Date getDateTime(String date) {
+        SimpleDateFormat dateFormat = new SimpleDateFormat(
+                Constants.DATE_FORMAT, Locale.getDefault());
+        Date date1 = new Date();
+        try {
+            date1 = dateFormat.parse(date);
+        } catch (ParseException e) {
+            Log.e(TAG, "Date parsing failed", e);
+        }
+        return date1;
+    }
+
+    public static HighlightImpl updateHighlightStyle(String rangy, String style) {
+        String query = "SELECT " + ID + " FROM " + TABLE_NAME + " WHERE " + COL_RANGY + " = '" + rangy + "'";
+        int id = DbAdapter.getIdForQuery(query);
+        if (id != -1 && update(id, updateRangy(rangy, style), style.replace("highlight_", ""))) {
+            return getHighlightId(id);
+        }
+        return null;
+    }
+
+    public static HighlightImpl getHighlightForRangy(String rangy) {
+        String query = "SELECT " + ID + " FROM " + TABLE_NAME + " WHERE " + COL_RANGY + " = '" + rangy + "'";
+        return getHighlightId(DbAdapter.getIdForQuery(query));
+    }
+
+    private static String updateRangy(String rangy, String style) {
+        /*Pattern p = Pattern.compile("\\highlight_\\w+");
+        Matcher m = p.matcher(rangy);
+        return m.replaceAll(style);*/
+        String[] s = rangy.split("\\$");
+        StringBuilder builder = new StringBuilder();
+        for (String p : s) {
+            if (TextUtils.isDigitsOnly(p)) {
+                builder.append(p);
+                builder.append('$');
+            } else {
+                builder.append(style);
+                builder.append('$');
+            }
+        }
+        return builder.toString();
+    }
+
+    private static boolean update(int id, String s, String color) {
+        HighlightImpl highlightImpl = getHighlightId(id);
+        highlightImpl.setRangy(s);
+        highlightImpl.setType(color);
+        return DbAdapter.updateHighLight(getHighlightContentValues(highlightImpl), String.valueOf(id));
+    }
+
+    public static void saveHighlightIfNotExists(HighLight highLight) {
+        String query = "SELECT " + ID + " FROM " + TABLE_NAME + " WHERE " + COL_UUID + " = '" + highLight.getUUID() + "'";
+        int id = DbAdapter.getIdForQuery(query);
+        if (id == -1) {
+            DbAdapter.saveHighLight(getHighlightContentValues(highLight));
+        }
+    }
+}
+
+
+
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/BaseMvpView.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/BaseMvpView.java
new file mode 100755 (executable)
index 0000000..c81d368
--- /dev/null
@@ -0,0 +1,9 @@
+package com.folioreader.ui.base;
+
+/**
+ * Created by gautam on 12/6/17.
+ */
+
+public interface BaseMvpView {
+    void onError();
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/DictionaryCallBack.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/DictionaryCallBack.java
new file mode 100755 (executable)
index 0000000..920b201
--- /dev/null
@@ -0,0 +1,15 @@
+package com.folioreader.ui.base;
+
+import com.folioreader.model.dictionary.Dictionary;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+
+public interface DictionaryCallBack extends BaseMvpView {
+
+    void onDictionaryDataReceived(Dictionary dictionary);
+
+    //TODO
+    void playMedia(String url);
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/DictionaryTask.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/DictionaryTask.java
new file mode 100755 (executable)
index 0000000..12e5e5a
--- /dev/null
@@ -0,0 +1,65 @@
+package com.folioreader.ui.base;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.folioreader.model.dictionary.Dictionary;
+import com.folioreader.util.AppUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+
+public class DictionaryTask extends AsyncTask<String, Void, Dictionary> {
+
+    private static final String TAG = "DictionaryTask";
+
+    private DictionaryCallBack callBack;
+
+    public DictionaryTask(DictionaryCallBack callBack) {
+        this.callBack = callBack;
+    }
+
+    @Override
+    protected Dictionary doInBackground(String... strings) {
+        String strUrl = strings[0];
+        try {
+            URL url = new URL(strUrl);
+            URLConnection urlConnection = url.openConnection();
+            InputStream inputStream = urlConnection.getInputStream();
+            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, AppUtil.charsetNameForURLConnection(urlConnection)));
+            StringBuilder stringBuilder = new StringBuilder();
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                stringBuilder.append(line);
+            }
+
+            ObjectMapper objectMapper = new ObjectMapper();
+            objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
+            return objectMapper.readValue(stringBuilder.toString(), Dictionary.class);
+        } catch (IOException e) {
+            Log.e(TAG, "DictionaryTask failed", e);
+        }
+        return null;
+    }
+
+    @Override
+    protected void onPostExecute(Dictionary dictionary) {
+        super.onPostExecute(dictionary);
+        if (dictionary != null) {
+            callBack.onDictionaryDataReceived(dictionary);
+        } else {
+            callBack.onError();
+        }
+        cancel(true);
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/HtmlTask.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/HtmlTask.java
new file mode 100755 (executable)
index 0000000..c890adf
--- /dev/null
@@ -0,0 +1,61 @@
+package com.folioreader.ui.base;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.folioreader.util.AppUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * Background async task which downloads the html content of a web page
+ * from server
+ *
+ * @author by gautam on 12/6/17.
+ */
+
+public class HtmlTask extends AsyncTask<String, Void, String> {
+
+    private static final String TAG = "HtmlTask";
+
+    private HtmlTaskCallback callback;
+
+    public HtmlTask(HtmlTaskCallback callback) {
+        this.callback = callback;
+    }
+
+    @Override
+    protected String doInBackground(String... urls) {
+        String strUrl = urls[0];
+        try {
+            URL url = new URL(strUrl);
+            URLConnection urlConnection = url.openConnection();
+            InputStream inputStream = urlConnection.getInputStream();
+            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, AppUtil.charsetNameForURLConnection(urlConnection)));
+            StringBuilder stringBuilder = new StringBuilder();
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                stringBuilder.append(line);
+            }
+            return stringBuilder.toString();
+        } catch (IOException e) {
+            Log.e(TAG, "HtmlTask failed", e);
+        }
+        return null;
+    }
+
+    @Override
+    protected void onPostExecute(String htmlString) {
+        if (htmlString != null) {
+            callback.onReceiveHtml(htmlString);
+        } else {
+            callback.onError();
+        }
+        cancel(true);
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/HtmlTaskCallback.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/HtmlTaskCallback.java
new file mode 100755 (executable)
index 0000000..8e2b700
--- /dev/null
@@ -0,0 +1,9 @@
+package com.folioreader.ui.base;
+
+/**
+ * @author gautam chibde on 12/6/17.
+ */
+
+public interface HtmlTaskCallback extends BaseMvpView {
+    void onReceiveHtml(String html);
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/HtmlUtil.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/HtmlUtil.java
new file mode 100755 (executable)
index 0000000..ee8a011
--- /dev/null
@@ -0,0 +1,147 @@
+package com.folioreader.ui.base;
+
+import android.content.Context;
+
+import com.folioreader.Config;
+import com.folioreader.Constants;
+import com.folioreader.R;
+
+/**
+ * @author gautam chibde on 14/6/17.
+ */
+
+public final class HtmlUtil {
+
+    /**
+     * Function modifies input html string by adding extra css,js and font information.
+     *
+     * @param context     Activity Context
+     * @param htmlContent input html raw data
+     * @return modified raw html string
+     */
+    public static String getHtmlContent(Context context, String htmlContent, Config config) {
+        String cssPath =
+                String.format(context.getString(R.string.css_tag), "file:///android_asset/css/Style.css");
+
+
+        String jsPath = String.format(context.getString(R.string.script_tag),
+                        "file:///android_asset/js/jsface.min.js");
+        jsPath =
+                jsPath + String.format(context.getString(R.string.script_tag),
+                        "file:///android_asset/js/jquery-3.1.1.min.js");
+
+        jsPath =
+                jsPath + String.format(context.getString(R.string.script_tag),
+                        "file:///android_asset/js/rangy-core.js");
+        jsPath =
+                jsPath + String.format(context.getString(R.string.script_tag),
+                        "file:///android_asset/js/rangy-highlighter.js");
+        jsPath =
+                jsPath + String.format(context.getString(R.string.script_tag),
+                        "file:///android_asset/js/rangy-classapplier.js");
+        jsPath =
+                jsPath + String.format(context.getString(R.string.script_tag),
+                        "file:///android_asset/js/rangy-serializer.js");
+        jsPath =
+                jsPath + String.format(context.getString(R.string.script_tag),
+                        "file:///android_asset/js/rangy-serializer.js");
+        jsPath =
+                jsPath + String.format(context.getString(R.string.script_tag),
+                        "file:///android_asset/js/Bridge.js");
+
+        jsPath =
+                jsPath + String.format(context.getString(R.string.script_tag),
+                        "file:///android_asset/android.selection.js");
+        jsPath =
+                jsPath + String.format(context.getString(R.string.script_tag_method_call),
+                        "setMediaOverlayStyleColors('#C0ED72','#C0ED72')");
+
+        String toInject = "\n" + cssPath + "\n" + jsPath + "\n</head>";
+        htmlContent = htmlContent.replace("</head>", toInject);
+
+        String classes = "";
+        switch (config.getFont()) {
+            case Constants.FONT_EBGARAMOND:
+                classes = "garamond";
+                break;
+            case Constants.FONT_LATO:
+                classes = "lato";
+                break;
+            case Constants.FONT_LORA:
+                classes = "lora";
+                break;
+            case Constants.FONT_RALEWAY:
+                classes = "raleway";
+                break;
+            default:
+                break;
+        }
+
+        if (config.isNightMode()) {
+            classes += " nightMode";
+        }
+
+        switch (config.getFontSize()) {
+            case 0:
+                classes += " textSizeOne";
+                break;
+            case 1:
+                classes += " textSizeTwo";
+                break;
+            case 2:
+                classes += " textSizeThree";
+                break;
+            case 3:
+                classes += " textSizeFour";
+                break;
+            case 4:
+                classes += " textSizeFive";
+                break;
+            default:
+                break;
+        }
+
+        switch (config.getMarginSize()) {
+            case 0:
+                classes += " marginSizeOne";
+                break;
+            case 1:
+                classes += " marginSizeTwo";
+                break;
+            case 2:
+                classes += " marginSizeThree";
+                break;
+            case 3:
+                classes += " marginSizeFour";
+                break;
+            case 4:
+                classes += " marginSizeFive";
+                break;
+            default:
+                break;
+        }
+
+        switch (config.getInterlineSize()) {
+            case 0:
+                classes += " interlineSizeOne";
+                break;
+            case 1:
+                classes += " interlineSizeTwo";
+                break;
+            case 2:
+                classes += " interlineSizeThree";
+                break;
+            case 3:
+                classes += " interlineSizeFour";
+                break;
+            case 4:
+                classes += " interlineSizeFive";
+                break;
+            default:
+                break;
+        }
+
+        htmlContent = htmlContent.replace("<html ", "<html class=\"" + classes + "\" ");
+        return htmlContent;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/ManifestCallBack.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/ManifestCallBack.java
new file mode 100755 (executable)
index 0000000..1d36970
--- /dev/null
@@ -0,0 +1,12 @@
+package com.folioreader.ui.base;
+
+import org.readium.r2_streamer.model.publication.EpubPublication;
+
+/**
+ * @author by gautam chibde on 12/6/17.
+ */
+
+public interface ManifestCallBack extends BaseMvpView {
+
+    void onReceivePublication(EpubPublication publication);
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/ManifestTask.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/ManifestTask.java
new file mode 100755 (executable)
index 0000000..c6e06bd
--- /dev/null
@@ -0,0 +1,83 @@
+package com.folioreader.ui.base;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.folioreader.util.AppUtil;
+
+import org.readium.r2_streamer.model.publication.EpubPublication;
+import org.readium.r2_streamer.model.tableofcontents.TOCLink;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * Background async task which makes API call to get Epub publication
+ * manifest from server
+ *
+ * @author by gautam on 12/6/17.
+ */
+
+public class ManifestTask extends AsyncTask<String, Void, EpubPublication> {
+
+    private static final String TAG = "ManifestTask";
+
+    private ManifestCallBack manifestCallBack;
+
+    public ManifestTask(ManifestCallBack manifestCallBack) {
+        this.manifestCallBack = manifestCallBack;
+    }
+
+    @Override
+    protected EpubPublication doInBackground(String... urls) {
+        String strUrl = urls[0];
+
+        try {
+            URL url = new URL(strUrl);
+            URLConnection urlConnection = url.openConnection();
+            InputStream inputStream = urlConnection.getInputStream();
+            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, AppUtil.charsetNameForURLConnection(urlConnection)));
+            StringBuilder stringBuilder = new StringBuilder();
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                stringBuilder.append(line);
+            }
+
+            ObjectMapper objectMapper = new ObjectMapper();
+            return objectMapper.readValue(stringBuilder.toString(), EpubPublication.class);
+        } catch (IOException e) {
+            Log.e(TAG, "ManifestTask failed", e);
+        }
+        return null;
+    }
+
+    @Override
+    protected void onPostExecute(EpubPublication publication) {
+        if (publication != null) {
+            //TODO can be implemented in r2-streamer?
+            if (publication.tableOfContents != null) {
+                for (TOCLink link : publication.tableOfContents) {
+                    setBookTitle(link, publication);
+                }
+            }
+            manifestCallBack.onReceivePublication(publication);
+        } else {
+            manifestCallBack.onError();
+        }
+        cancel(true);
+    }
+
+    private void setBookTitle(TOCLink link, EpubPublication publication) {
+        for (int i = 0; i < publication.spines.size(); i++) {
+            if (publication.spines.get(i).href.equals(link.href)) {
+                publication.spines.get(i).bookTitle = link.bookTitle;
+                return;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/OnSaveHighlight.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/OnSaveHighlight.java
new file mode 100755 (executable)
index 0000000..1f00524
--- /dev/null
@@ -0,0 +1,10 @@
+package com.folioreader.ui.base;
+
+/**
+ * Created by gautam on 10/10/17.
+ */
+
+public interface OnSaveHighlight {
+
+    void onFinished();
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/SaveReceivedHighlightTask.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/SaveReceivedHighlightTask.java
new file mode 100755 (executable)
index 0000000..6ac4a13
--- /dev/null
@@ -0,0 +1,39 @@
+package com.folioreader.ui.base;
+
+import android.os.AsyncTask;
+
+import com.folioreader.model.HighLight;
+import com.folioreader.model.sqlite.HighLightTable;
+
+import java.util.List;
+
+/**
+ * Background task to save received highlights.
+ * <p>
+ * Created by gautam on 10/10/17.
+ */
+public class SaveReceivedHighlightTask extends AsyncTask<Void, Void, Void> {
+
+    private OnSaveHighlight onSaveHighlight;
+    private List<HighLight> highLights;
+
+    public SaveReceivedHighlightTask(OnSaveHighlight onSaveHighlight,
+                                     List<HighLight> highLights) {
+        this.onSaveHighlight = onSaveHighlight;
+        this.highLights = highLights;
+    }
+
+    @Override
+    protected Void doInBackground(Void... voids) {
+        for (HighLight highLight : highLights) {
+            HighLightTable.saveHighlightIfNotExists(highLight);
+        }
+        return null;
+    }
+
+    @Override
+    protected void onPostExecute(Void aVoid) {
+        super.onPostExecute(aVoid);
+        onSaveHighlight.onFinished();
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/WikipediaCallBack.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/WikipediaCallBack.java
new file mode 100755 (executable)
index 0000000..f71844e
--- /dev/null
@@ -0,0 +1,15 @@
+package com.folioreader.ui.base;
+
+import com.folioreader.model.dictionary.Wikipedia;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+
+public interface WikipediaCallBack extends BaseMvpView {
+
+    void onWikipediaDataReceived(Wikipedia wikipedia);
+
+    //TODO
+    void playMedia(String url);
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/base/WikipediaTask.java b/Android/folioreader/src/main/java/com/folioreader/ui/base/WikipediaTask.java
new file mode 100755 (executable)
index 0000000..67b7180
--- /dev/null
@@ -0,0 +1,87 @@
+package com.folioreader.ui.base;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.folioreader.model.dictionary.Wikipedia;
+import com.folioreader.util.AppUtil;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+
+public class WikipediaTask extends AsyncTask<String, Void, Wikipedia> {
+
+    private static final String TAG = "WikipediaTask";
+
+    private WikipediaCallBack callBack;
+
+    public WikipediaTask(WikipediaCallBack callBack) {
+        this.callBack = callBack;
+    }
+
+    @Override
+    protected Wikipedia doInBackground(String... strings) {
+        String strUrl = strings[0];
+        try {
+            URL url = new URL(strUrl);
+            URLConnection urlConnection = url.openConnection();
+            InputStream inputStream = urlConnection.getInputStream();
+            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, AppUtil.charsetNameForURLConnection(urlConnection)));
+            StringBuilder stringBuilder = new StringBuilder();
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                stringBuilder.append(line);
+            }
+
+            try {
+                JSONArray array = new JSONArray(stringBuilder.toString());
+
+                if (array.length() == 4) {
+                    try {
+                        Wikipedia wikipedia = new Wikipedia();
+                        wikipedia.setWord(array.get(0).toString());
+                        JSONArray defs = (JSONArray) array.get(2);
+                        wikipedia.setDefinition(defs.get(0).toString());
+                        JSONArray links = (JSONArray) array.get(3);
+                        wikipedia.setLink(links.get(0).toString());
+                        return wikipedia;
+                    } catch (Exception e) {
+                        Log.e(TAG, "WikipediaTask failed", e);
+                        return null;
+                    }
+
+                } else {
+                    return null;
+                }
+            } catch (JSONException e) {
+                Log.e(TAG, "WikipediaTask failed", e);
+                return null;
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "WikipediaTask failed", e);
+        }
+        return null;
+    }
+
+    @Override
+    protected void onPostExecute(Wikipedia wikipedia) {
+        super.onPostExecute(wikipedia);
+        if (wikipedia != null) {
+            callBack.onWikipediaDataReceived(wikipedia);
+        } else {
+            callBack.onError();
+        }
+        cancel(true);
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/activity/ContentHighlightActivity.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/activity/ContentHighlightActivity.java
new file mode 100755 (executable)
index 0000000..8bf678a
--- /dev/null
@@ -0,0 +1,105 @@
+package com.folioreader.ui.folio.activity;\r
+\r
+import android.graphics.Color;\r
+import android.os.Bundle;\r
+import android.support.v4.app.FragmentTransaction;\r
+import android.support.v4.content.ContextCompat;\r
+import android.support.v7.app.AppCompatActivity;\r
+import android.view.View;\r
+import android.widget.ImageView;\r
+import android.widget.TextView;\r
+\r
+import com.folioreader.Config;\r
+import com.folioreader.Constants;\r
+import com.folioreader.R;\r
+import com.folioreader.ui.folio.fragment.HighlightFragment;\r
+import com.folioreader.ui.tableofcontents.view.TableOfContentFragment;\r
+import com.folioreader.util.AppUtil;\r
+import com.folioreader.util.FolioReader;\r
+import com.folioreader.util.UiUtil;\r
+\r
+public class ContentHighlightActivity extends AppCompatActivity {\r
+    private boolean mIsNightMode;\r
+    private Config mConfig;\r
+\r
+    @Override\r
+    protected void onCreate(Bundle savedInstanceState) {\r
+        super.onCreate(savedInstanceState);\r
+        setContentView(R.layout.activity_content_highlight);\r
+        getSupportActionBar().hide();\r
+        mConfig = AppUtil.getSavedConfig(this);\r
+        mIsNightMode = mConfig.isNightMode();\r
+        initViews();\r
+    }\r
+\r
+    private void initViews() {\r
+        findViewById(R.id.layout_content_highlights).setVisibility(View.GONE);\r
+        // findViewById(R.id.layout_content_highlights).setBackgroundDrawable(UiUtil.getShapeDrawable(this, mConfig.getThemeColor()));\r
+        if (mIsNightMode) {\r
+            findViewById(R.id.toolbar).setBackgroundColor(Color.BLACK);\r
+            UiUtil.setColorToImage(this, mConfig.getThemeColor(), ((ImageView) findViewById(R.id.btn_close)).getDrawable());\r
+            ((TextView) findViewById(R.id.tvTitle)).setTextColor(UiUtil.getColorList(this,mConfig.getThemeColor(), mConfig.getThemeColor()));\r
+\r
+            findViewById(R.id.btn_contents).setBackgroundDrawable(UiUtil.convertColorIntoStateDrawable(this, mConfig.getThemeColor(), R.color.black));\r
+            findViewById(R.id.btn_highlights).setBackgroundDrawable(UiUtil.convertColorIntoStateDrawable(this, mConfig.getThemeColor(), R.color.black));\r
+            ((TextView) findViewById(R.id.btn_contents)).setTextColor(UiUtil.getColorList(this, R.color.black, mConfig.getThemeColor()));\r
+            ((TextView) findViewById(R.id.btn_highlights)).setTextColor(UiUtil.getColorList(this, R.color.black, mConfig.getThemeColor()));\r
+\r
+        } else {\r
+            findViewById(R.id.toolbar).setBackgroundColor(ContextCompat.getColor(this, mConfig.getToolbarColor()));\r
+            UiUtil.setColorToImage(this, mConfig.getIconColor(), ((ImageView) findViewById(R.id.btn_close)).getDrawable());\r
+            ((TextView) findViewById(R.id.tvTitle)).setTextColor(UiUtil.getColorList(this,mConfig.getIconColor(), mConfig.getIconColor()));\r
+\r
+            ((TextView) findViewById(R.id.btn_contents)).setTextColor(UiUtil.getColorList(this, R.color.white, mConfig.getThemeColor()));\r
+            ((TextView) findViewById(R.id.btn_highlights)).setTextColor(UiUtil.getColorList(this, R.color.white, mConfig.getThemeColor()));\r
+            findViewById(R.id.btn_contents).setBackgroundDrawable(UiUtil.convertColorIntoStateDrawable(this, mConfig.getThemeColor(), R.color.white));\r
+            findViewById(R.id.btn_highlights).setBackgroundDrawable(UiUtil.convertColorIntoStateDrawable(this, mConfig.getThemeColor(), R.color.white));\r
+        }\r
+\r
+\r
+        loadContentFragment();\r
+        findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                finish();\r
+            }\r
+        });\r
+\r
+        findViewById(R.id.btn_contents).setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                loadContentFragment();\r
+            }\r
+        });\r
+\r
+        findViewById(R.id.btn_highlights).setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                loadHighlightsFragment();\r
+            }\r
+        });\r
+    }\r
+\r
+    private void loadContentFragment() {\r
+        findViewById(R.id.btn_contents).setSelected(true);\r
+        findViewById(R.id.btn_highlights).setSelected(false);\r
+        TableOfContentFragment contentFrameLayout\r
+                = TableOfContentFragment.newInstance(getIntent().getStringExtra(Constants.CHAPTER_SELECTED),\r
+                getIntent().getStringExtra(Constants.BOOK_TITLE));\r
+        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();\r
+        ft.replace(R.id.parent, contentFrameLayout);\r
+        ft.commit();\r
+    }\r
+\r
+    private void loadHighlightsFragment() {\r
+        findViewById(R.id.btn_contents).setSelected(false);\r
+        findViewById(R.id.btn_highlights).setSelected(true);\r
+        String bookId = getIntent().getStringExtra(FolioReader.INTENT_BOOK_ID);\r
+        String bookTitle= getIntent().getStringExtra(Constants.BOOK_TITLE);\r
+        HighlightFragment highlightFragment = HighlightFragment.newInstance(bookId, bookTitle);\r
+        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();\r
+        ft.replace(R.id.parent, highlightFragment);\r
+        ft.commit();\r
+    }\r
+\r
+}\r
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/activity/FolioActivity.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/activity/FolioActivity.java
new file mode 100755 (executable)
index 0000000..aa5f7bf
--- /dev/null
@@ -0,0 +1,656 @@
+/*\r
+* Copyright (C) 2016 Pedro Paulo de Amorim\r
+*\r
+* Licensed under the Apache License, Version 2.0 (the "License");\r
+* you may not use this file except in compliance with the License.\r
+* You may obtain a copy of the License at\r
+*\r
+* http://www.apache.org/licenses/LICENSE-2.0\r
+*\r
+* Unless required by applicable law or agreed to in writing, software\r
+* distributed under the License is distributed on an "AS IS" BASIS,\r
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+* See the License for the specific language governing permissions and\r
+* limitations under the License.\r
+*/\r
+package com.folioreader.ui.folio.activity;\r
+\r
+import android.Manifest;\r
+import android.annotation.TargetApi;\r
+import android.content.Context;\r
+import android.content.Intent;\r
+import android.content.pm.PackageManager;\r
+import android.os.Build;\r
+import android.os.Bundle;\r
+import android.support.annotation.NonNull;\r
+import android.support.v4.app.ActivityCompat;\r
+import android.support.v4.content.ContextCompat;\r
+import android.support.v7.app.AppCompatActivity;\r
+import android.support.v7.widget.Toolbar;\r
+import android.text.Html;\r
+import android.util.Log;\r
+import android.view.View;\r
+import android.view.animation.Animation;\r
+import android.view.animation.AnimationUtils;\r
+import android.widget.ImageButton;\r
+import android.widget.RelativeLayout;\r
+import android.widget.TextView;\r
+import android.widget.Toast;\r
+\r
+import com.folioreader.Config;\r
+import com.folioreader.Constants;\r
+import com.folioreader.R;\r
+import com.folioreader.model.HighlightImpl;\r
+import com.folioreader.model.event.AnchorIdEvent;\r
+import com.folioreader.model.event.BusOwner;\r
+import com.folioreader.model.event.MediaOverlayHighlightStyleEvent;\r
+import com.folioreader.model.event.MediaOverlayPlayPauseEvent;\r
+import com.folioreader.model.event.MediaOverlaySpeedEvent;\r
+import com.folioreader.model.event.WebViewPosition;\r
+import com.folioreader.ui.folio.adapter.FolioPageFragmentAdapter;\r
+import com.folioreader.ui.folio.fragment.FolioPageFragment;\r
+import com.folioreader.ui.folio.presenter.MainMvpView;\r
+import com.folioreader.ui.folio.presenter.MainPresenter;\r
+import com.folioreader.util.AppUtil;\r
+import com.folioreader.util.FileUtil;\r
+import com.folioreader.util.FolioReader;\r
+import com.folioreader.util.UiUtil;\r
+import com.folioreader.view.ConfigBottomSheetDialogFragment;\r
+import com.folioreader.view.DirectionalViewpager;\r
+import com.folioreader.view.ObservableWebView;\r
+import com.folioreader.view.StyleableTextView;\r
+import com.squareup.otto.Bus;\r
+import com.squareup.otto.ThreadEnforcer;\r
+\r
+import org.readium.r2_streamer.model.container.Container;\r
+import org.readium.r2_streamer.model.container.EpubContainer;\r
+import org.readium.r2_streamer.model.publication.EpubPublication;\r
+import org.readium.r2_streamer.model.publication.link.Link;\r
+import org.readium.r2_streamer.server.EpubServer;\r
+import org.readium.r2_streamer.server.EpubServerSingleton;\r
+\r
+import java.io.IOException;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+\r
+import static com.folioreader.Constants.CHAPTER_SELECTED;\r
+import static com.folioreader.Constants.HIGHLIGHT_SELECTED;\r
+import static com.folioreader.Constants.SELECTED_CHAPTER_POSITION;\r
+import static com.folioreader.Constants.TYPE;\r
+\r
+public class FolioActivity\r
+        extends AppCompatActivity\r
+        implements FolioPageFragment.FolioPageFragmentCallback,\r
+        ObservableWebView.ToolBarListener,\r
+        ConfigBottomSheetDialogFragment.ConfigDialogCallback,\r
+        BusOwner,\r
+        MainMvpView {\r
+\r
+    private static final String TAG = "FolioActivity";\r
+\r
+    public static final String INTENT_EPUB_SOURCE_PATH = "com.folioreader.epub_asset_path";\r
+    public static final String INTENT_EPUB_SOURCE_TYPE = "epub_source_type";\r
+    public static final String INTENT_HIGHLIGHTS_LIST = "highlight_list";\r
+       public static final String PARAM_FILE_NAME = "PARAM_FILE_NAME";\r
+       public static final String PARAM_CURRENT_CHAPTER = "PARAM_CURRENT_CHAPTER";\r
+       public static final String PARAM_CHAPTERS_COUNT = "PARAM_CHAPTERS_COUNT";\r
+\r
+       public enum EpubSourceType {\r
+        RAW,\r
+        ASSETS,\r
+        SD_CARD\r
+    }\r
+\r
+    private boolean isOpen = true;\r
+\r
+    public static final int ACTION_CONTENT_HIGHLIGHT = 77;\r
+    private String bookFileName;\r
+    private static final String HIGHLIGHT_ITEM = "highlight_item";\r
+\r
+    private final Bus BUS = new Bus(ThreadEnforcer.MAIN);\r
+    @Override\r
+    public Bus getBus() {\r
+        return BUS;\r
+    }\r
+\r
+    public boolean mIsActionBarVisible;\r
+    private DirectionalViewpager mFolioPageViewPager;\r
+    private Toolbar mToolbar;\r
+\r
+    private int mChapterPosition;\r
+    private FolioPageFragmentAdapter mFolioPageFragmentAdapter;\r
+    private int mWebViewScrollPosition;\r
+    private ConfigBottomSheetDialogFragment mConfigBottomSheetDialogFragment;\r
+    private TextView title;\r
+\r
+    private List<Link> mSpineReferenceList = new ArrayList<>();\r
+    private EpubServer mEpubServer;\r
+\r
+    private Animation slide_down;\r
+    private Animation slide_up;\r
+    private boolean mIsNightMode;\r
+    private Config mConfig;\r
+    private String mBookId;\r
+    private String mEpubFilePath;\r
+    private EpubSourceType mEpubSourceType;\r
+    int mEpubRawId = 0;\r
+\r
+    @Override\r
+    protected void onCreate(Bundle savedInstanceState) {\r
+        super.onCreate(savedInstanceState);\r
+        setContentView(R.layout.folio_activity);\r
+\r
+        mBookId = getIntent().getStringExtra(FolioReader.INTENT_BOOK_ID);\r
+        mEpubSourceType = (EpubSourceType)\r
+                getIntent().getExtras().getSerializable(FolioActivity.INTENT_EPUB_SOURCE_TYPE);\r
+        if (mEpubSourceType.equals(EpubSourceType.RAW)) {\r
+            mEpubRawId = getIntent().getExtras().getInt(FolioActivity.INTENT_EPUB_SOURCE_PATH);\r
+        } else {\r
+            mEpubFilePath = getIntent().getExtras()\r
+                    .getString(FolioActivity.INTENT_EPUB_SOURCE_PATH);\r
+        }\r
+\r
+        setConfig();\r
+\r
+        if (!mConfig.isShowTts()) {\r
+            findViewById(R.id.btn_speaker).setVisibility(View.GONE);\r
+        }\r
+\r
+        title = (TextView) findViewById(R.id.lbl_center);\r
+        slide_down = AnimationUtils.loadAnimation(getApplicationContext(),\r
+                R.anim.slide_down);\r
+        slide_up = AnimationUtils.loadAnimation(getApplicationContext(),\r
+                R.anim.slide_up);\r
+\r
+        mToolbar = (Toolbar) findViewById(R.id.toolbar);\r
+\r
+        BUS.register(this);\r
+\r
+\r
+        if (ContextCompat.checkSelfPermission(FolioActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {\r
+            ActivityCompat.requestPermissions(FolioActivity.this, Constants.getWriteExternalStoragePerms(), Constants.WRITE_EXTERNAL_STORAGE_REQUEST);\r
+        } else {\r
+            setupBook();\r
+        }\r
+\r
+        initAudioView();\r
+\r
+        findViewById(R.id.btn_drawer).setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                Intent intent = new Intent(FolioActivity.this, ContentHighlightActivity.class);\r
+                intent.putExtra(CHAPTER_SELECTED, mSpineReferenceList.get(mChapterPosition).href);\r
+                intent.putExtra(FolioReader.INTENT_BOOK_ID, mBookId);\r
+                intent.putExtra(Constants.BOOK_TITLE, bookFileName);\r
+                startActivityForResult(intent, ACTION_CONTENT_HIGHLIGHT);\r
+                overridePendingTransition(R.anim.slide_in_up, R.anim.slide_out_up);\r
+            }\r
+        });\r
+\r
+        // speaker = (ImageView) findViewById(R.id.btn_speaker);\r
+        findViewById(R.id.btn_speaker).setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                if (isOpen) {\r
+                    audioContainer.startAnimation(slide_up);\r
+                    audioContainer.setVisibility(View.VISIBLE);\r
+                    shade.setVisibility(View.VISIBLE);\r
+                } else {\r
+                    audioContainer.startAnimation(slide_down);\r
+                    audioContainer.setVisibility(View.INVISIBLE);\r
+                    shade.setVisibility(View.GONE);\r
+                }\r
+                isOpen = !isOpen;\r
+            }\r
+        });\r
+\r
+        mIsNightMode = mConfig.isNightMode();\r
+        if (mIsNightMode) {\r
+            audioContainer.setBackgroundColor(ContextCompat.getColor(FolioActivity.this, R.color.night));\r
+        }\r
+        ToolbarUtils.updateToolbarColors(this, mToolbar, mConfig, mIsNightMode);\r
+    }\r
+\r
+    private void initBook(String mEpubFileName, int mEpubRawId, String mEpubFilePath, EpubSourceType mEpubSourceType) {\r
+        try {\r
+            int portNumber = getIntent().getIntExtra(Config.INTENT_PORT, Constants.PORT_NUMBER);\r
+            mEpubServer = EpubServerSingleton.getEpubServerInstance(portNumber);\r
+            mEpubServer.start();\r
+            String path = FileUtil.saveEpubFileAndLoadLazyBook(FolioActivity.this, mEpubSourceType, mEpubFilePath,\r
+                    mEpubRawId, mEpubFileName);\r
+            addEpub(path);\r
+\r
+            String urlString = Constants.LOCALHOST + bookFileName + "/manifest";\r
+            new MainPresenter(this).parseManifest(urlString);\r
+\r
+        } catch (IOException e) {\r
+            Log.e(TAG, "initBook failed", e);\r
+        }\r
+    }\r
+\r
+    private void addEpub(String path) throws IOException {\r
+        Container epubContainer = new EpubContainer(path);\r
+        mEpubServer.addEpub(epubContainer, "/" + bookFileName);\r
+        getEpubResource();\r
+    }\r
+\r
+    private void getEpubResource() {\r
+    }\r
+\r
+    @Override\r
+    protected void onPostCreate(Bundle savedInstanceState) {\r
+        super.onPostCreate(savedInstanceState);\r
+        configDrawerLayoutButtons();\r
+    }\r
+\r
+    @Override\r
+    public void onBackPressed() {\r
+        saveBookState();\r
+           Intent data = new Intent();\r
+           data.putExtra(PARAM_FILE_NAME, bookFileName);\r
+           data.putExtra(PARAM_CURRENT_CHAPTER,mFolioPageViewPager.getCurrentItem() + 1);\r
+           data.putExtra(PARAM_CHAPTERS_COUNT, mFolioPageViewPager.getExpectedAdapterCount());\r
+           setResult(RESULT_OK, data);\r
+        finish();\r
+    }\r
+\r
+    @Override\r
+    public void onOrientationChange(int orentation) {\r
+        if (orentation == 0) {\r
+            mFolioPageViewPager.setDirection(DirectionalViewpager.Direction.VERTICAL);\r
+            mFolioPageFragmentAdapter =\r
+                    new FolioPageFragmentAdapter(getSupportFragmentManager(),\r
+                            mSpineReferenceList, bookFileName, mBookId);\r
+            mFolioPageViewPager.setAdapter(mFolioPageFragmentAdapter);\r
+            mFolioPageViewPager.setOffscreenPageLimit(1);\r
+            mFolioPageViewPager.setCurrentItem(mChapterPosition);\r
+\r
+        } else {\r
+            mFolioPageViewPager.setDirection(DirectionalViewpager.Direction.HORIZONTAL);\r
+            mFolioPageFragmentAdapter =\r
+                    new FolioPageFragmentAdapter(getSupportFragmentManager(),\r
+                            mSpineReferenceList, bookFileName, mBookId);\r
+            mFolioPageViewPager.setAdapter(mFolioPageFragmentAdapter);\r
+            mFolioPageViewPager.setCurrentItem(mChapterPosition);\r
+        }\r
+    }\r
+\r
+    private void configFolio() {\r
+        mFolioPageViewPager = (DirectionalViewpager) findViewById(R.id.folioPageViewPager);\r
+        mFolioPageViewPager.setOnPageChangeListener(new DirectionalViewpager.OnPageChangeListener() {\r
+            @Override\r
+            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {\r
+            }\r
+\r
+            @Override\r
+            public void onPageSelected(int position) {\r
+                BUS.post(new MediaOverlayPlayPauseEvent(mSpineReferenceList.get(mChapterPosition).href, false, true));\r
+                mPlayPauseBtn.setImageDrawable(ContextCompat.getDrawable(FolioActivity.this, R.drawable.play_icon));\r
+                mChapterPosition = position;\r
+            }\r
+\r
+            @Override\r
+            public void onPageScrollStateChanged(int state) {\r
+//                if (state == DirectionalViewpager.SCROLL_STATE_IDLE) {\r
+//                    title.setText(mSpineReferenceList.get(mChapterPosition).bookTitle);\r
+//                }\r
+            }\r
+        });\r
+\r
+        if (mSpineReferenceList != null) {\r
+            mFolioPageFragmentAdapter = new FolioPageFragmentAdapter(getSupportFragmentManager(), mSpineReferenceList, bookFileName, mBookId);\r
+            mFolioPageViewPager.setAdapter(mFolioPageFragmentAdapter);\r
+        }\r
+\r
+        if (AppUtil.checkPreviousBookStateExist(FolioActivity.this, bookFileName)) {\r
+            mFolioPageViewPager.setCurrentItem(AppUtil.getPreviousBookStatePosition(FolioActivity.this, bookFileName));\r
+        }\r
+    }\r
+\r
+    private void configDrawerLayoutButtons() {\r
+        findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                onBackPressed();\r
+            }\r
+        });\r
+\r
+        findViewById(R.id.btn_config).setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                mConfigBottomSheetDialogFragment = new ConfigBottomSheetDialogFragment();\r
+                mConfigBottomSheetDialogFragment.show(getSupportFragmentManager(), mConfigBottomSheetDialogFragment.getTag());\r
+            }\r
+        });\r
+    }\r
+\r
+    private void saveBookState() {\r
+        if (mSpineReferenceList.size() > 0) {\r
+            AppUtil.saveBookState(FolioActivity.this, bookFileName, mFolioPageViewPager.getCurrentItem(), mFolioPageViewPager\r
+                    .getExpectedAdapterCount(), mWebViewScrollPosition);\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public void hideOrshowToolBar() {\r
+//        if (mIsActionBarVisible) {\r
+//            toolbarAnimateHide();\r
+//        } else {\r
+//            toolbarAnimateShow();\r
+//        }\r
+    }\r
+\r
+    @Override\r
+    public void hideToolBarIfVisible() {\r
+//        if (mIsActionBarVisible) {\r
+//            toolbarAnimateHide();\r
+//        }\r
+    }\r
+\r
+    @Override\r
+    public void setPagerToPosition(String href) {\r
+    }\r
+\r
+    @Override\r
+    public void setLastWebViewPosition(int position) {\r
+        this.mWebViewScrollPosition = position;\r
+    }\r
+\r
+    @Override\r
+    public void goToChapter(String href) {\r
+        String spineHref = href.substring(href.indexOf(bookFileName + "/") + bookFileName.length() + 1);\r
+           String anchorHref = new String(spineHref);\r
+        if (href.contains("#")) {\r
+            spineHref = spineHref.substring(0, spineHref.indexOf("#"));\r
+        }\r
+        for (Link spine : mSpineReferenceList) {\r
+            if (spine.href.contains(spineHref)) {\r
+                mChapterPosition = mSpineReferenceList.indexOf(spine);\r
+                mFolioPageViewPager.setCurrentItem(mChapterPosition);\r
+                title.setText(spine.getChapterTitle());\r
+                BUS.post(new AnchorIdEvent(anchorHref));\r
+                break;\r
+            }\r
+        }\r
+    }\r
+\r
+    private void toolbarAnimateShow() {\r
+//        if (!mIsActionBarVisible) {\r
+//            mToolbar.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start();\r
+//            mIsActionBarVisible = true;\r
+//        }\r
+    }\r
+\r
+    private void toolbarAnimateHide() {\r
+//        mIsActionBarVisible = false;\r
+//        mToolbar.animate().translationY(-mToolbar.getHeight()).setInterpolator(new AccelerateInterpolator(2)).start();\r
+    }\r
+\r
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\r
+    private void toolbarSetElevation(float elevation) {\r
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\r
+            mToolbar.setElevation(elevation);\r
+        }\r
+    }\r
+\r
+    public HighlightImpl setCurrentPagerPostion(HighlightImpl highlightImpl) {\r
+//        highlight.setCurrentPagerPostion(mFolioPageViewPager.getCurrentItem());\r
+        return highlightImpl;\r
+    }\r
+\r
+    @Override\r
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\r
+        if (requestCode == ACTION_CONTENT_HIGHLIGHT && resultCode == RESULT_OK && data.hasExtra(TYPE)) {\r
+\r
+            String type = data.getStringExtra(TYPE);\r
+            if (type.equals(CHAPTER_SELECTED)) {\r
+                String selectedChapterHref = data.getStringExtra(SELECTED_CHAPTER_POSITION);\r
+                for (Link spine : mSpineReferenceList) {\r
+                    if (selectedChapterHref.contains(spine.href)) {\r
+                        mChapterPosition = mSpineReferenceList.indexOf(spine);\r
+                        mFolioPageViewPager.setCurrentItem(mChapterPosition);\r
+                        title.setText(data.getStringExtra(Constants.BOOK_TITLE));\r
+                        BUS.post(new AnchorIdEvent(selectedChapterHref));\r
+                        break;\r
+                    }\r
+                }\r
+            } else if (type.equals(HIGHLIGHT_SELECTED)) {\r
+                HighlightImpl highlightImpl = data.getParcelableExtra(HIGHLIGHT_ITEM);\r
+                int position = highlightImpl.getPageNumber();\r
+                mFolioPageViewPager.setCurrentItem(position);\r
+                BUS.post(new WebViewPosition(mSpineReferenceList.get(mChapterPosition).href, highlightImpl.getRangy()));\r
+            }\r
+        }\r
+    }\r
+\r
+    @Override\r
+    protected void onDestroy() {\r
+        super.onDestroy();\r
+        if (mEpubServer != null) {\r
+            mEpubServer.stop();\r
+        }\r
+        BUS.unregister(this);\r
+    }\r
+\r
+    public int getmChapterPosition() {\r
+        return mChapterPosition;\r
+    }\r
+\r
+    @Override\r
+    public void onLoadPublication(EpubPublication publication) {\r
+        mSpineReferenceList.addAll(publication.spines);\r
+        if (publication.metadata.title != null) {\r
+            title.setText(publication.metadata.title);\r
+        }\r
+\r
+        if (mBookId == null) {\r
+            if (publication.metadata.identifier != null) {\r
+                mBookId = publication.metadata.identifier;\r
+            } else {\r
+                if (publication.metadata.title != null) {\r
+                    mBookId = String.valueOf(publication.metadata.title.hashCode());\r
+                } else {\r
+                    mBookId = String.valueOf(bookFileName.hashCode());\r
+                }\r
+            }\r
+        }\r
+        configFolio();\r
+    }\r
+\r
+    private void setConfig() {\r
+        if (AppUtil.getSavedConfig(this) != null) {\r
+            mConfig = AppUtil.getSavedConfig(this);\r
+        } else if (getIntent().getParcelableExtra(Config.INTENT_CONFIG) != null) {\r
+            mConfig = getIntent().getParcelableExtra(Config.INTENT_CONFIG);\r
+            AppUtil.saveConfig(this, mConfig);\r
+        } else {\r
+            mConfig = new Config.ConfigBuilder().build();\r
+            AppUtil.saveConfig(this, mConfig);\r
+        }\r
+    }\r
+\r
+\r
+    //*************************************************************************//\r
+    //                           AUDIO PLAYER                                  //\r
+    //*************************************************************************//\r
+    private StyleableTextView mHalfSpeed, mOneSpeed, mTwoSpeed, mOneAndHalfSpeed;\r
+    private StyleableTextView mBackgroundColorStyle, mUnderlineStyle, mTextColorStyle;\r
+    private RelativeLayout audioContainer;\r
+    private boolean mIsSpeaking;\r
+    private ImageButton mPlayPauseBtn, mPreviousButton, mNextButton;\r
+    private RelativeLayout shade;\r
+\r
+    private void initAudioView() {\r
+        mHalfSpeed = (StyleableTextView) findViewById(R.id.btn_half_speed);\r
+        mOneSpeed = (StyleableTextView) findViewById(R.id.btn_one_x_speed);\r
+        mTwoSpeed = (StyleableTextView) findViewById(R.id.btn_twox_speed);\r
+        audioContainer = (RelativeLayout) findViewById(R.id.container);\r
+        shade = (RelativeLayout) findViewById(R.id.shade);\r
+        mOneAndHalfSpeed = (StyleableTextView) findViewById(R.id.btn_one_and_half_speed);\r
+        mPlayPauseBtn = (ImageButton) findViewById(R.id.play_button);\r
+        mPreviousButton = (ImageButton) findViewById(R.id.prev_button);\r
+        mNextButton = (ImageButton) findViewById(R.id.next_button);\r
+        mBackgroundColorStyle = (StyleableTextView) findViewById(R.id.btn_backcolor_style);\r
+        mUnderlineStyle = (StyleableTextView) findViewById(R.id.btn_text_undeline_style);\r
+        mTextColorStyle = (StyleableTextView) findViewById(R.id.btn_text_color_style);\r
+        mIsSpeaking = false;\r
+\r
+        final Context mContext = mHalfSpeed.getContext();\r
+        mOneAndHalfSpeed.setText(Html.fromHtml(mContext.getString(R.string.one_and_half_speed)));\r
+        mHalfSpeed.setText(Html.fromHtml(mContext.getString(R.string.half_speed_text)));\r
+        String styleUnderline =\r
+                mHalfSpeed.getContext().getResources().getString(R.string.style_underline);\r
+        mUnderlineStyle.setText(Html.fromHtml(styleUnderline));\r
+\r
+        setupColors(mContext);\r
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\r
+            findViewById(R.id.playback_speed_Layout).setVisibility(View.GONE);\r
+        }\r
+\r
+        shade.setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                if (isOpen) {\r
+                    audioContainer.startAnimation(slide_up);\r
+                    audioContainer.setVisibility(View.VISIBLE);\r
+                    shade.setVisibility(View.VISIBLE);\r
+                } else {\r
+                    audioContainer.startAnimation(slide_down);\r
+                    audioContainer.setVisibility(View.INVISIBLE);\r
+                    shade.setVisibility(View.GONE);\r
+                }\r
+                isOpen = !isOpen;\r
+            }\r
+        });\r
+\r
+        mPlayPauseBtn.setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                if (mIsSpeaking) {\r
+                    BUS.post(new MediaOverlayPlayPauseEvent(mSpineReferenceList.get(mChapterPosition).href, false, false));\r
+                    mPlayPauseBtn.setImageDrawable(ContextCompat.getDrawable(FolioActivity.this, R.drawable.play_icon));\r
+                    UiUtil.setColorToImage(mContext, mConfig.getThemeColor(), mPlayPauseBtn.getDrawable());\r
+                } else {\r
+                    BUS.post(new MediaOverlayPlayPauseEvent(mSpineReferenceList.get(mChapterPosition).href, true, false));\r
+                    mPlayPauseBtn.setImageDrawable(ContextCompat.getDrawable(FolioActivity.this, R.drawable.pause_btn));\r
+                    UiUtil.setColorToImage(mContext, mConfig.getThemeColor(), mPlayPauseBtn.getDrawable());\r
+                }\r
+                mIsSpeaking = !mIsSpeaking;\r
+            }\r
+        });\r
+\r
+        mHalfSpeed.setOnClickListener(new View.OnClickListener() {\r
+            @TargetApi(Build.VERSION_CODES.M)\r
+            @Override\r
+            public void onClick(View v) {\r
+                mHalfSpeed.setSelected(true);\r
+                mOneSpeed.setSelected(false);\r
+                mOneAndHalfSpeed.setSelected(false);\r
+                mTwoSpeed.setSelected(false);\r
+                BUS.post(new MediaOverlaySpeedEvent(MediaOverlaySpeedEvent.Speed.HALF));\r
+            }\r
+        });\r
+\r
+        mOneSpeed.setOnClickListener(new View.OnClickListener() {\r
+            @TargetApi(Build.VERSION_CODES.M)\r
+            @Override\r
+            public void onClick(View v) {\r
+                mHalfSpeed.setSelected(false);\r
+                mOneSpeed.setSelected(true);\r
+                mOneAndHalfSpeed.setSelected(false);\r
+                mTwoSpeed.setSelected(false);\r
+                BUS.post(new MediaOverlaySpeedEvent(MediaOverlaySpeedEvent.Speed.ONE));\r
+            }\r
+        });\r
+        mOneAndHalfSpeed.setOnClickListener(new View.OnClickListener() {\r
+            @TargetApi(Build.VERSION_CODES.M)\r
+            @Override\r
+            public void onClick(View v) {\r
+                mHalfSpeed.setSelected(false);\r
+                mOneSpeed.setSelected(false);\r
+                mOneAndHalfSpeed.setSelected(true);\r
+                mTwoSpeed.setSelected(false);\r
+                BUS.post(new MediaOverlaySpeedEvent(MediaOverlaySpeedEvent.Speed.ONE_HALF));\r
+            }\r
+        });\r
+        mTwoSpeed.setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                mHalfSpeed.setSelected(false);\r
+                mOneSpeed.setSelected(false);\r
+                mOneAndHalfSpeed.setSelected(false);\r
+                mTwoSpeed.setSelected(true);\r
+                BUS.post(new MediaOverlaySpeedEvent(MediaOverlaySpeedEvent.Speed.TWO));\r
+            }\r
+        });\r
+\r
+        mBackgroundColorStyle.setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                mBackgroundColorStyle.setSelected(true);\r
+                mUnderlineStyle.setSelected(false);\r
+                mTextColorStyle.setSelected(false);\r
+                BUS.post(new MediaOverlayHighlightStyleEvent(MediaOverlayHighlightStyleEvent.Style.DEFAULT));\r
+            }\r
+        });\r
+\r
+        mUnderlineStyle.setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                mBackgroundColorStyle.setSelected(false);\r
+                mUnderlineStyle.setSelected(true);\r
+                mTextColorStyle.setSelected(false);\r
+                BUS.post(new MediaOverlayHighlightStyleEvent(MediaOverlayHighlightStyleEvent.Style.UNDERLINE));\r
+\r
+            }\r
+        });\r
+\r
+        mTextColorStyle.setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+                mBackgroundColorStyle.setSelected(false);\r
+                mUnderlineStyle.setSelected(false);\r
+                mTextColorStyle.setSelected(true);\r
+                BUS.post(new MediaOverlayHighlightStyleEvent(MediaOverlayHighlightStyleEvent.Style.BACKGROUND));\r
+            }\r
+        });\r
+\r
+    }\r
+\r
+    private void setupColors(Context context) {\r
+        mHalfSpeed.setTextColor(UiUtil.getColorList(context, mConfig.getThemeColor(), R.color.grey_color));\r
+        mOneAndHalfSpeed.setTextColor(UiUtil.getColorList(context, mConfig.getThemeColor(), R.color.grey_color));\r
+        mTwoSpeed.setTextColor(UiUtil.getColorList(context, mConfig.getThemeColor(), R.color.grey_color));\r
+        mOneSpeed.setTextColor(UiUtil.getColorList(context, mConfig.getThemeColor(), R.color.grey_color));\r
+        mUnderlineStyle.setTextColor(UiUtil.getColorList(context, mConfig.getThemeColor(), R.color.grey_color));\r
+        mBackgroundColorStyle.setTextColor(UiUtil.getColorList(context, R.color.white, R.color.grey_color));\r
+        mBackgroundColorStyle.setBackgroundDrawable(UiUtil.convertColorIntoStateDrawable(this, mConfig.getThemeColor(), android.R.color.transparent));\r
+        mTextColorStyle.setTextColor(UiUtil.getColorList(context, mConfig.getThemeColor(), R.color.grey_color));\r
+        UiUtil.setColorToImage(context, mConfig.getThemeColor(), mPlayPauseBtn.getDrawable());\r
+        UiUtil.setColorToImage(context, mConfig.getThemeColor(), mNextButton.getDrawable());\r
+        UiUtil.setColorToImage(context, mConfig.getThemeColor(), mPreviousButton.getDrawable());\r
+    }\r
+\r
+    @Override\r
+    public void onError() {\r
+    }\r
+\r
+    private void setupBook() {\r
+        bookFileName = FileUtil.getEpubFilename(this, mEpubSourceType, mEpubFilePath, mEpubRawId);\r
+        initBook(bookFileName, mEpubRawId, mEpubFilePath, mEpubSourceType);\r
+    }\r
+\r
+    @Override\r
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {\r
+        switch (requestCode) {\r
+            case Constants.WRITE_EXTERNAL_STORAGE_REQUEST:\r
+                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {\r
+                    setupBook();\r
+                } else {\r
+                    Toast.makeText(this, getString(R.string.cannot_access_epub_message), Toast.LENGTH_LONG).show();\r
+                    finish();\r
+                }\r
+                break;\r
+        }\r
+    }\r
+}\r
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/activity/ToolbarUtils.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/activity/ToolbarUtils.java
new file mode 100644 (file)
index 0000000..4d7c71f
--- /dev/null
@@ -0,0 +1,36 @@
+package com.folioreader.ui.folio.activity;
+
+import android.content.Context;
+import android.support.annotation.ColorRes;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.Toolbar;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.folioreader.Config;
+import com.folioreader.R;
+import com.folioreader.util.UiUtil;
+
+/**
+ * @author golonkos
+ */
+
+public final class ToolbarUtils {
+
+       public static void updateToolbarColors(Context context, Toolbar toolbar, Config config, boolean nightMode) {
+               if (nightMode) {
+                       setToolbarColors(context, toolbar, config.getThemeColor(), R.color.black);
+               } else {
+                       setToolbarColors(context, toolbar, config.getIconColor(), config.getThemeColor());
+               }
+       }
+
+       private static void setToolbarColors(Context context, Toolbar toolbar, @ColorRes int iconColor, @ColorRes int toolbarColor) {
+               UiUtil.setColorToImage(context, iconColor, ((ImageView) toolbar.findViewById(R.id.btn_close)).getDrawable());
+               UiUtil.setColorToImage(context, iconColor, ((ImageView) toolbar.findViewById(R.id.btn_drawer)).getDrawable());
+               UiUtil.setColorToImage(context, iconColor, ((ImageView) toolbar.findViewById(R.id.btn_config)).getDrawable());
+               UiUtil.setColorToImage(context, iconColor, ((ImageView) toolbar.findViewById(R.id.btn_speaker)).getDrawable());
+               toolbar.setBackgroundColor(ContextCompat.getColor(context, toolbarColor));
+               ((TextView) toolbar.findViewById(R.id.lbl_center)).setTextColor(ContextCompat.getColor(context, iconColor));
+       }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/DictionaryAdapter.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/DictionaryAdapter.java
new file mode 100755 (executable)
index 0000000..1ad8e11
--- /dev/null
@@ -0,0 +1,152 @@
+package com.folioreader.ui.folio.adapter;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.support.v7.widget.RecyclerView;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.folioreader.R;
+import com.folioreader.model.dictionary.Audio;
+import com.folioreader.model.dictionary.DictionaryResults;
+import com.folioreader.model.dictionary.Example;
+import com.folioreader.model.dictionary.Pronunciations;
+import com.folioreader.model.dictionary.Senses;
+import com.folioreader.ui.base.DictionaryCallBack;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+
+public class DictionaryAdapter extends RecyclerView.Adapter<DictionaryAdapter.DictionaryHolder> {
+
+    private List<DictionaryResults> results;
+    private Context context;
+    private DictionaryCallBack callBack;
+
+    public DictionaryAdapter(Context context, DictionaryCallBack callBack) {
+        this.results = new ArrayList<>();
+        this.context = context;
+        this.callBack = callBack;
+    }
+
+    @Override
+    public DictionaryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        return new DictionaryHolder(LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.item_dictionary, parent, false));
+    }
+
+    @Override
+    public void onBindViewHolder(DictionaryHolder holder, int position) {
+        final DictionaryResults res = results.get(position);
+        if (res.getPartOfSpeech() != null) {
+            int wordLength = res.getHeadword().length();
+            SpannableString spannableString = new SpannableString(res.getHeadword() + " - " + res.getPartOfSpeech());
+            spannableString.setSpan(new StyleSpan(Typeface.BOLD), 0, wordLength, 0);
+            spannableString.setSpan(new StyleSpan(Typeface.ITALIC), wordLength + 2, spannableString.length(), 0);
+            holder.name.setText(spannableString);
+        } else {
+            holder.name.setTypeface(Typeface.DEFAULT_BOLD);
+            holder.name.setText(res.getHeadword());
+        }
+        StringBuilder def = new StringBuilder();
+        StringBuilder exp = new StringBuilder();
+
+        if (res.getSenses() != null) {
+            for (Senses senses : res.getSenses()) {
+                if (senses.getDefinition() != null) {
+                    for (String s : senses.getDefinition()) {
+                        def.append("\u2022 ").append(s).append('\n');
+                    }
+                }
+            }
+
+            for (Senses senses : res.getSenses()) {
+                if (senses.getExamples() != null) {
+                    for (Example s : senses.getExamples()) {
+                        exp.append("\u2022 ").append(s.getText()).append('\n');
+                    }
+                }
+            }
+        }
+        if (!def.toString().trim().isEmpty()) {
+            def.insert(0, "Definition\n");
+            holder.definition.setText(def.toString());
+        } else {
+            holder.definition.setVisibility(View.GONE);
+        }
+
+        if (!exp.toString().trim().isEmpty()) {
+            exp.insert(0, "Example\n");
+            holder.example.setText(exp.toString());
+        } else {
+            holder.example.setVisibility(View.GONE);
+        }
+//        if (res.getPronunciations() != null) {
+//            final String url = getAudioUrl(res.getPronunciations());
+//            if (url == null) {
+//                holder.sound.setVisibility(View.GONE);
+//            }
+//        }
+
+//        holder.sound.setOnClickListener(new View.OnClickListener() {
+//            @Override
+//            public void onClick(View v) {
+//                Log.i("DictionaryAdapter", "clicked");
+//                if (res.getPronunciations() != null) {
+//                    final String url = getAudioUrl(res.getPronunciations());
+//                    callBack.playMedia(url);
+//                }
+//            }
+//        });
+    }
+
+    private String getAudioUrl(List<Pronunciations> pronunciations) {
+        if (!pronunciations.isEmpty()
+                && pronunciations.get(0).getAudio() != null
+                && !pronunciations.get(0).getAudio().isEmpty()) {
+            Audio audio = pronunciations.get(0).getAudio().get(0);
+            if (audio.getUrl() != null) {
+                return audio.getUrl();
+            }
+        }
+        return null;
+    }
+
+    public void setResults(List<DictionaryResults> resultsList) {
+        if(resultsList != null && !resultsList.isEmpty()) {
+            results.addAll(resultsList);
+            notifyDataSetChanged();
+        }
+    }
+
+    public void clear() {
+        results.clear();
+        notifyItemRangeRemoved(0, results.size());
+    }
+
+    @Override
+    public int getItemCount() {
+        return results.size();
+    }
+
+    public static class DictionaryHolder extends RecyclerView.ViewHolder {
+        private TextView name, definition, example;
+        //TODO private ImageButton sound;
+
+        public DictionaryHolder(View itemView) {
+            super(itemView);
+            name = (TextView) itemView.findViewById(R.id.tv_word);
+            //sound = (ImageButton) itemView.findViewById(R.id.ib_speak);
+            definition = (TextView) itemView.findViewById(R.id.tv_definition);
+            example = (TextView) itemView.findViewById(R.id.tv_examples);
+        }
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/FolioPageFragmentAdapter.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/FolioPageFragmentAdapter.java
new file mode 100755 (executable)
index 0000000..7dce8b2
--- /dev/null
@@ -0,0 +1,39 @@
+package com.folioreader.ui.folio.adapter;
+
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+
+import com.folioreader.ui.folio.fragment.FolioPageFragment;
+
+import org.readium.r2_streamer.model.publication.link.Link;
+
+import java.util.List;
+
+/**
+ * @author mahavir on 4/2/16.
+ */
+public class FolioPageFragmentAdapter extends FragmentStatePagerAdapter {
+    private List<Link> mSpineReferences;
+    private String mEpubFileName;
+    private String mBookId;
+
+    public FolioPageFragmentAdapter(FragmentManager fm, List<Link> spineReferences, String epubFileName, String bookId) {
+        super(fm);
+        this.mSpineReferences = spineReferences;
+        this.mEpubFileName = epubFileName;
+        this.mBookId = bookId;
+    }
+
+    @Override
+    public Fragment getItem(int position) {
+        FolioPageFragment mFolioPageFragment = FolioPageFragment.newInstance(position, mEpubFileName, mSpineReferences.get(position),mBookId);
+        mFolioPageFragment.setFragmentPos(position);
+        return mFolioPageFragment;
+    }
+
+    @Override
+    public int getCount() {
+        return mSpineReferences.size();
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/FontAdapter.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/FontAdapter.java
new file mode 100755 (executable)
index 0000000..b01b431
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+* Copyright (C) 2016 Pedro Paulo de Amorim
+*
+* 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.folioreader.ui.folio.adapter;
+
+import com.folioreader.Font;
+import com.folioreader.R;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+public class FontAdapter extends RecyclerView.Adapter<FontAdapter.ViewHolder> {
+
+    private ArrayList<Font> mFonts = null;
+
+    @Override
+    public FontAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int i) {
+        return new ViewHolder(LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.row_font, parent, false));
+    }
+
+    @Override
+    public void onBindViewHolder(ViewHolder viewHolder, int i) {
+        viewHolder.mName.setText(mFonts.get(i).getName());
+    }
+
+    @Override
+    public int getItemCount() {
+        return mFonts != null ? mFonts.size() : 0;
+    }
+
+    public void setFonts(ArrayList<Font> mFonts) {
+        this.mFonts = mFonts;
+    }
+
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+
+        private TextView mName;
+
+        public ViewHolder(View v) {
+            super(v);
+            mName = (TextView) v.findViewById(R.id.name);
+        }
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/HighlightAdapter.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/adapter/HighlightAdapter.java
new file mode 100755 (executable)
index 0000000..9242747
--- /dev/null
@@ -0,0 +1,176 @@
+package com.folioreader.ui.folio.adapter;
+
+import android.content.Context;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.RecyclerView;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.folioreader.Config;
+import com.folioreader.R;
+import com.folioreader.model.HighlightImpl;
+import com.folioreader.util.AppUtil;
+import com.folioreader.util.UiUtil;
+import com.folioreader.view.UnderlinedTextView;
+
+import java.util.List;
+
+/**
+ * @author gautam chibde on 16/6/17.
+ */
+
+public class HighlightAdapter extends RecyclerView.Adapter<HighlightAdapter.HighlightHolder> {
+    private List<HighlightImpl> highlights;
+    private HighLightAdapterCallback callback;
+    private Context context;
+    private  Config config;
+
+    public HighlightAdapter(Context context, List<HighlightImpl> highlights, HighLightAdapterCallback callback, Config config) {
+        this.context = context;
+        this.highlights = highlights;
+        this.callback = callback;
+        this.config = config;
+    }
+
+    @Override
+    public HighlightHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        return new HighlightHolder(LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.row_highlight, parent, false));
+    }
+
+    @Override
+    public void onBindViewHolder(final HighlightHolder holder, final int position) {
+
+        holder.container.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                ((AppCompatActivity) context).runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        holder.container.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,FrameLayout.LayoutParams.WRAP_CONTENT));
+                    }
+                });
+            }
+        }, 10);
+
+        holder.content.setText(Html.fromHtml(getItem(position).getContent()));
+        UiUtil.setBackColorToTextView(holder.content,
+                getItem(position).getType());
+        holder.date.setText(AppUtil.formatDate(getItem(position).getDate()));
+        holder.container.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                callback.onItemClick(getItem(position));
+            }
+        });
+        holder.delete.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                callback.deleteHighlight(getItem(position).getId());
+                highlights.remove(position);
+                notifyDataSetChanged();
+
+            }
+        });
+        holder.editNote.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                callback.editNote(getItem(position), position);
+            }
+        });
+        if (getItem(position).getNote() != null) {
+            if (getItem(position).getNote().isEmpty()) {
+                holder.note.setVisibility(View.GONE);
+            } else {
+                holder.note.setVisibility(View.VISIBLE);
+                holder.note.setText(getItem(position).getNote());
+            }
+        } else {
+            holder.note.setVisibility(View.GONE);
+        }
+        holder.container.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                final int height = holder.container.getHeight();
+                ((AppCompatActivity) context).runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        ViewGroup.LayoutParams params =
+                                holder.swipeLinearLayout.getLayoutParams();
+                        params.height = height;
+                        holder.swipeLinearLayout.setLayoutParams(params);
+                    }
+                });
+            }
+        }, 30);
+        if (config.isNightMode()) {
+            holder.container.setBackgroundColor(ContextCompat.getColor(context,
+                    R.color.black));
+            holder.note.setTextColor(ContextCompat.getColor(context,
+                    R.color.white));
+            holder.date.setTextColor(ContextCompat.getColor(context,
+                    R.color.white));
+            holder.content.setTextColor(ContextCompat.getColor(context,
+                    R.color.white));
+        } else {
+            holder.container.setBackgroundColor(ContextCompat.getColor(context,
+                    R.color.white));
+            holder.note.setTextColor(ContextCompat.getColor(context,
+                    R.color.black));
+            holder.date.setTextColor(ContextCompat.getColor(context,
+                    R.color.black));
+            holder.content.setTextColor(ContextCompat.getColor(context,
+                    R.color.black));
+        }
+    }
+
+    private HighlightImpl getItem(int position) {
+        return highlights.get(position);
+    }
+
+    @Override
+    public int getItemCount() {
+        return highlights.size();
+    }
+
+    public void editNote(String note, int position) {
+        highlights.get(position).setNote(note);
+        notifyDataSetChanged();
+    }
+
+    static class HighlightHolder extends RecyclerView.ViewHolder {
+        private UnderlinedTextView content;
+        private ImageView delete, editNote;
+        private TextView date;
+        private RelativeLayout container;
+        private TextView note;
+        private LinearLayout swipeLinearLayout;
+
+        HighlightHolder(View itemView) {
+            super(itemView);
+            container = (RelativeLayout) itemView.findViewById(R.id.container);
+            swipeLinearLayout = (LinearLayout) itemView.findViewById(R.id.swipe_linear_layout);
+            content = (UnderlinedTextView) itemView.findViewById(R.id.utv_highlight_content);
+            delete = (ImageView) itemView.findViewById(R.id.iv_delete);
+            editNote = (ImageView) itemView.findViewById(R.id.iv_edit_note);
+            date = (TextView) itemView.findViewById(R.id.tv_highlight_date);
+            note = (TextView) itemView.findViewById(R.id.tv_note);
+        }
+    }
+
+    public interface HighLightAdapterCallback {
+        void onItemClick(HighlightImpl highlightImpl);
+
+        void deleteHighlight(int id);
+
+        void editNote(HighlightImpl highlightImpl, int position);
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/fragment/DictionaryFragment.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/fragment/DictionaryFragment.java
new file mode 100755 (executable)
index 0000000..4d4b42a
--- /dev/null
@@ -0,0 +1,225 @@
+package com.folioreader.ui.folio.fragment;
+
+import android.app.Dialog;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.media.MediaPlayer;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.folioreader.Constants;
+import com.folioreader.R;
+import com.folioreader.model.dictionary.Dictionary;
+import com.folioreader.model.dictionary.Wikipedia;
+import com.folioreader.ui.base.DictionaryCallBack;
+import com.folioreader.ui.base.DictionaryTask;
+import com.folioreader.ui.base.WikipediaCallBack;
+import com.folioreader.ui.base.WikipediaTask;
+import com.folioreader.ui.folio.adapter.DictionaryAdapter;
+
+import java.io.IOException;
+
+/**
+ * @author gautam chibde on 4/7/17.
+ */
+
+public class DictionaryFragment extends DialogFragment implements DictionaryCallBack, WikipediaCallBack {
+
+    private static final String TAG = "DictionaryFragment";
+
+    private String word;
+
+    private MediaPlayer mediaPlayer;
+    private RecyclerView dictResults;
+    private TextView noNetwork, dictionary, wikipedia, wikiWord, def;
+    private ProgressBar progressBar;
+    private Button googleSearch;
+    private LinearLayout wikiLayout;
+    private WebView wikiWebView;
+    private DictionaryAdapter mAdapter;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setStyle(STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog);
+        word = getArguments().getString(Constants.SELECTED_WORD);
+        mediaPlayer = new MediaPlayer();
+    }
+
+    @Override
+    public void onActivityCreated(Bundle arg0) {
+        super.onActivityCreated(arg0);
+        getDialog().getWindow().getAttributes().windowAnimations = R.style.DialogAnimation;
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.layout_dictionary, container);
+    }
+
+    @Override
+    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        noNetwork = (TextView) view.findViewById(R.id.no_network);
+        progressBar = (ProgressBar) view.findViewById(R.id.progress);
+        dictResults = (RecyclerView) view.findViewById(R.id.rv_dict_results);
+
+        googleSearch = (Button) view.findViewById(R.id.btn_google_search);
+        dictionary = (TextView) view.findViewById(R.id.btn_dictionary);
+        wikipedia = (TextView) view.findViewById(R.id.btn_wikipedia);
+
+        wikiLayout = (LinearLayout) view.findViewById(R.id.ll_wiki);
+        wikiWord = (TextView) view.findViewById(R.id.tv_word);
+        def = (TextView) view.findViewById(R.id.tv_def);
+        wikiWebView = (WebView) view.findViewById(R.id.wv_wiki);
+        wikiWebView.getSettings().setLoadsImagesAutomatically(true);
+        wikiWebView.setWebViewClient(new WebViewClient());
+        wikiWebView.getSettings().setJavaScriptEnabled(true);
+        wikiWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
+
+        dictionary.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                loadDictionary();
+            }
+        });
+
+        wikipedia.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                loadWikipedia();
+            }
+        });
+
+        googleSearch.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
+                intent.putExtra(SearchManager.QUERY, word);
+                startActivity(intent);
+            }
+        });
+
+        view.findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                dismiss();
+            }
+        });
+        dictResults.setLayoutManager(new LinearLayoutManager(getActivity()));
+        mAdapter = new DictionaryAdapter(getActivity(), this);
+        loadDictionary();
+    }
+
+    private void loadDictionary() {
+        if(noNetwork.getVisibility() == View.VISIBLE || googleSearch.getVisibility() == View.VISIBLE) {
+            noNetwork.setVisibility(View.GONE);
+            googleSearch.setVisibility(View.GONE);
+        }
+        wikiWebView.loadUrl("about:blank");
+        mAdapter.clear();
+        dictionary.setSelected(true);
+        wikipedia.setSelected(false);
+        wikiLayout.setVisibility(View.GONE);
+        dictResults.setVisibility(View.VISIBLE);
+        DictionaryTask task = new DictionaryTask(this);
+        String baseUrl = Constants.DICTIONARY_BASE_URL + word.trim();
+        task.execute(baseUrl);
+    }
+
+    private void loadWikipedia() {
+        if(noNetwork.getVisibility() == View.VISIBLE || googleSearch.getVisibility() == View.VISIBLE) {
+            noNetwork.setVisibility(View.GONE);
+            googleSearch.setVisibility(View.GONE);
+        }
+        wikiWebView.loadUrl("about:blank");
+        mAdapter.clear();
+        wikiLayout.setVisibility(View.VISIBLE);
+        dictResults.setVisibility(View.GONE);
+        dictionary.setSelected(false);
+        wikipedia.setSelected(true);
+        WikipediaTask task = new WikipediaTask(this);
+        task.execute(Constants.WIKIPEDIA_API_URL + word.trim());
+    }
+
+    @Override
+    public void onError() {
+        noNetwork.setVisibility(View.VISIBLE);
+        progressBar.setVisibility(View.GONE);
+        noNetwork.setText("offline");
+        googleSearch.setVisibility(View.GONE);
+    }
+
+    @Override
+    public void onDictionaryDataReceived(Dictionary dictionary) {
+        progressBar.setVisibility(View.GONE);
+        if (dictionary.getResults().isEmpty()) {
+            noNetwork.setVisibility(View.VISIBLE);
+            googleSearch.setVisibility(View.VISIBLE);
+            noNetwork.setText("Word not found");
+        } else {
+            mAdapter.setResults(dictionary.getResults());
+            dictResults.setAdapter(mAdapter);
+        }
+    }
+
+    @Override
+    public void onWikipediaDataReceived(Wikipedia wikipedia) {
+        wikiWord.setText(wikipedia.getWord());
+        if (wikipedia.getDefinition().trim().isEmpty()) {
+            def.setVisibility(View.GONE);
+        } else {
+            String definition = "\"" +
+                    wikipedia.getDefinition() +
+                    "\"";
+            def.setText(definition);
+        }
+        wikiWebView.loadUrl(wikipedia.getLink());
+    }
+
+    //TODO
+    @Override
+    public void playMedia(String url) {
+        if (mediaPlayer != null) {
+            try {
+                mediaPlayer.setDataSource(url);
+                mediaPlayer.prepare();
+                mediaPlayer.start();
+            } catch (IOException e) {
+                Log.e(TAG, "playMedia failed", e);
+            }
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        Dialog d = getDialog();
+        if (d != null) {
+            d.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mediaPlayer.isPlaying()) {
+            mediaPlayer.stop();
+            mediaPlayer.release();
+        }
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/fragment/FolioPageFragment.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/fragment/FolioPageFragment.java
new file mode 100755 (executable)
index 0000000..e5bdbfd
--- /dev/null
@@ -0,0 +1,1010 @@
+package com.folioreader.ui.folio.fragment;\r
+\r
+import android.app.Activity;\r
+import android.annotation.SuppressLint;\r
+import android.content.Intent;\r
+import android.content.res.Configuration;\r
+import android.graphics.Color;\r
+import android.graphics.PorterDuff;\r
+import android.graphics.drawable.Drawable;\r
+import android.net.Uri;\r
+import android.os.Build;\r
+import android.os.Bundle;\r
+import android.support.v4.app.Fragment;\r
+import android.support.v4.content.ContextCompat;\r
+import android.text.TextUtils;\r
+import android.util.Log;\r
+import android.view.LayoutInflater;\r
+import android.view.View;\r
+import android.view.ViewGroup;\r
+import android.view.animation.Animation;\r
+import android.view.animation.AnimationUtils;\r
+import android.webkit.JavascriptInterface;\r
+import android.webkit.JsResult;\r
+import android.webkit.WebChromeClient;\r
+import android.webkit.WebResourceRequest;\r
+import android.webkit.WebResourceResponse;\r
+import android.webkit.WebView;\r
+import android.webkit.WebViewClient;\r
+import android.widget.TextView;\r
+import android.widget.Toast;\r
+\r
+import com.bossturban.webviewmarker.TextSelectionSupport;\r
+import com.folioreader.Config;\r
+import com.folioreader.Constants;\r
+import com.folioreader.R;\r
+import com.folioreader.model.HighLight;\r
+import com.folioreader.model.HighlightImpl;\r
+import com.folioreader.model.event.AnchorIdEvent;\r
+import com.folioreader.model.event.BusOwner;\r
+import com.folioreader.model.event.MediaOverlayHighlightStyleEvent;\r
+import com.folioreader.model.event.MediaOverlayPlayPauseEvent;\r
+import com.folioreader.model.event.MediaOverlaySpeedEvent;\r
+import com.folioreader.model.event.ReloadDataEvent;\r
+import com.folioreader.model.event.RewindIndexEvent;\r
+import com.folioreader.model.event.WebViewPosition;\r
+import com.folioreader.model.quickaction.ActionItem;\r
+import com.folioreader.model.quickaction.QuickAction;\r
+import com.folioreader.model.sqlite.HighLightTable;\r
+import com.folioreader.ui.base.HtmlTask;\r
+import com.folioreader.ui.base.HtmlTaskCallback;\r
+import com.folioreader.ui.base.HtmlUtil;\r
+import com.folioreader.ui.folio.activity.FolioActivity;\r
+import com.folioreader.ui.folio.mediaoverlay.MediaController;\r
+import com.folioreader.ui.folio.mediaoverlay.MediaControllerCallbacks;\r
+import com.folioreader.util.AppUtil;\r
+import com.folioreader.util.FolioReader;\r
+import com.folioreader.util.HighlightUtil;\r
+import com.folioreader.util.SMILParser;\r
+import com.folioreader.util.UiUtil;\r
+import com.folioreader.view.ObservableWebView;\r
+import com.folioreader.view.VerticalSeekbar;\r
+import com.squareup.otto.Bus;\r
+import com.squareup.otto.Subscribe;\r
+\r
+import org.readium.r2_streamer.model.publication.link.Link;\r
+\r
+import java.io.UnsupportedEncodingException;\r
+import java.net.URLDecoder;\r
+import java.util.Locale;\r
+import java.util.regex.Matcher;\r
+import java.util.regex.Pattern;\r
+\r
+/**\r
+ * Created by mahavir on 4/2/16.\r
+ */\r
+@SuppressWarnings("PMD.AvoidDuplicateLiterals")\r
+public class FolioPageFragment extends Fragment implements HtmlTaskCallback, MediaControllerCallbacks, ObservableWebView.SeekBarListener {\r
+\r
+    public static final String KEY_FRAGMENT_FOLIO_POSITION = "com.folioreader.ui.folio.fragment.FolioPageFragment.POSITION";\r
+    public static final String KEY_FRAGMENT_FOLIO_BOOK_TITLE = "com.folioreader.ui.folio.fragment.FolioPageFragment.BOOK_TITLE";\r
+    public static final String KEY_FRAGMENT_EPUB_FILE_NAME = "com.folioreader.ui.folio.fragment.FolioPageFragment.EPUB_FILE_NAME";\r
+    private static final String KEY_IS_SMIL_AVAILABLE = "com.folioreader.ui.folio.fragment.FolioPageFragment.IS_SMIL_AVAILABLE";\r
+    public static final String TAG = FolioPageFragment.class.getSimpleName();\r
+\r
+    private static final int ACTION_ID_COPY = 1001;\r
+    private static final int ACTION_ID_SHARE = 1002;\r
+    private static final int ACTION_ID_HIGHLIGHT = 1003;\r
+    private static final int ACTION_ID_DEFINE = 1004;\r
+\r
+    private static final int ACTION_ID_HIGHLIGHT_COLOR = 1005;\r
+    private static final int ACTION_ID_DELETE = 1006;\r
+\r
+    private static final int ACTION_ID_HIGHLIGHT_YELLOW = 1007;\r
+    private static final int ACTION_ID_HIGHLIGHT_GREEN = 1008;\r
+    private static final int ACTION_ID_HIGHLIGHT_BLUE = 1009;\r
+    private static final int ACTION_ID_HIGHLIGHT_PINK = 1010;\r
+    private static final int ACTION_ID_HIGHLIGHT_UNDERLINE = 1011;\r
+    private static final String KEY_TEXT_ELEMENTS = "text_elements";\r
+    private static final String SPINE_ITEM = "spine_item";\r
+\r
+    private String mHtmlString = null;\r
+    private boolean hasMediaOverlay = false;\r
+    private String mAnchorId;\r
+    private String rangy = "";\r
+    private String highlightId;\r
+\r
+    public interface FolioPageFragmentCallback {\r
+\r
+        void setPagerToPosition(String href);\r
+\r
+        void setLastWebViewPosition(int position);\r
+\r
+        void goToChapter(String href);\r
+    }\r
+\r
+    private View mRootView;\r
+\r
+    private VerticalSeekbar mScrollSeekbar;\r
+    private ObservableWebView mWebview;\r
+    private TextSelectionSupport mTextSelectionSupport;\r
+    private TextView mPagesLeftTextView, mMinutesLeftTextView;\r
+    private FolioPageFragmentCallback mActivityCallback;\r
+\r
+    private int mScrollY;\r
+    private int mTotalMinutes;\r
+    private String mSelectedText;\r
+    private Animation mFadeInAnimation, mFadeOutAnimation;\r
+\r
+    private Link spineItem;\r
+    private int mPosition = -1;\r
+    private String mBookTitle;\r
+    private String mEpubFileName = null;\r
+    private int mPos;\r
+    private boolean mIsPageReloaded;\r
+    private int mLastWebviewScrollpos;\r
+\r
+    private String highlightStyle;\r
+\r
+    private MediaController mediaController;\r
+    private Config mConfig;\r
+    private String mBookId;\r
+\r
+    public static FolioPageFragment newInstance(int position, String bookTitle, Link spineRef, String bookId) {\r
+        FolioPageFragment fragment = new FolioPageFragment();\r
+        Bundle args = new Bundle();\r
+        args.putInt(KEY_FRAGMENT_FOLIO_POSITION, position);\r
+        args.putString(KEY_FRAGMENT_FOLIO_BOOK_TITLE, bookTitle);\r
+        args.putString(FolioReader.INTENT_BOOK_ID, bookId);\r
+        args.putSerializable(SPINE_ITEM, spineRef);\r
+        fragment.setArguments(args);\r
+        return fragment;\r
+    }\r
+\r
+    @Override\r
+    public View onCreateView(LayoutInflater inflater,\r
+                             ViewGroup container, Bundle savedInstanceState) {\r
+        if ((savedInstanceState != null)\r
+                && savedInstanceState.containsKey(KEY_FRAGMENT_FOLIO_POSITION)\r
+                && savedInstanceState.containsKey(KEY_FRAGMENT_FOLIO_BOOK_TITLE)) {\r
+            mPosition = savedInstanceState.getInt(KEY_FRAGMENT_FOLIO_POSITION);\r
+            mBookTitle = savedInstanceState.getString(KEY_FRAGMENT_FOLIO_BOOK_TITLE);\r
+            mEpubFileName = savedInstanceState.getString(KEY_FRAGMENT_EPUB_FILE_NAME);\r
+            mBookId = getArguments().getString(FolioReader.INTENT_BOOK_ID);\r
+            spineItem = (Link) savedInstanceState.getSerializable(SPINE_ITEM);\r
+        } else {\r
+            mPosition = getArguments().getInt(KEY_FRAGMENT_FOLIO_POSITION);\r
+            mBookTitle = getArguments().getString(KEY_FRAGMENT_FOLIO_BOOK_TITLE);\r
+            mEpubFileName = getArguments().getString(KEY_FRAGMENT_EPUB_FILE_NAME);\r
+            spineItem = (Link) getArguments().getSerializable(SPINE_ITEM);\r
+            mBookId = getArguments().getString(FolioReader.INTENT_BOOK_ID);\r
+        }\r
+        if (spineItem != null) {\r
+            if (spineItem.properties.contains("media-overlay")) {\r
+                mediaController = new MediaController(getActivity(), MediaController.MediaType.SMIL, this);\r
+                hasMediaOverlay = true;\r
+            } else {\r
+                mediaController = new MediaController(getActivity(), MediaController.MediaType.TTS, this);\r
+                mediaController.setTextToSpeech(getActivity());\r
+            }\r
+        }\r
+        highlightStyle = HighlightImpl.HighlightStyle.classForStyle(HighlightImpl.HighlightStyle.Normal);\r
+        mRootView = View.inflate(getActivity(), R.layout.folio_page_fragment, null);\r
+        mPagesLeftTextView = (TextView) mRootView.findViewById(R.id.pagesLeft);\r
+        mMinutesLeftTextView = (TextView) mRootView.findViewById(R.id.minutesLeft);\r
+\r
+        Activity activity = getActivity();\r
+\r
+        mConfig = AppUtil.getSavedConfig(activity);\r
+\r
+        if (activity instanceof FolioPageFragmentCallback)\r
+            mActivityCallback = (FolioPageFragmentCallback) activity;\r
+\r
+        if (activity instanceof BusOwner)\r
+            ((BusOwner) activity).getBus().register(this);\r
+\r
+        initSeekbar();\r
+        initAnimations();\r
+        initWebView();\r
+        updatePagesLeftTextBg();\r
+\r
+        return mRootView;\r
+    }\r
+\r
+\r
+    private String getWebviewUrl() {\r
+        return Constants.LOCALHOST + mBookTitle + "/" + spineItem.href;\r
+    }\r
+\r
+    @Override\r
+    public void onConfigurationChanged(Configuration newConfig) {\r
+        super.onConfigurationChanged(newConfig);\r
+        float positionTopView = mWebview.getTop();\r
+        float contentHeight = mWebview.getContentHeight();\r
+        float currentScrollPosition = mScrollY;\r
+        float percentWebview = (currentScrollPosition - positionTopView) / contentHeight;\r
+        float webviewsize = mWebview.getContentHeight() - mWebview.getTop();\r
+        float positionInWV = webviewsize * percentWebview;\r
+        int positionY = Math.round(mWebview.getTop() + positionInWV);\r
+        mScrollY = positionY;\r
+    }\r
+\r
+    /**\r
+     * [EVENT BUS FUNCTION]\r
+     * Function triggered from {@link FolioActivity#initAudioView()} when pause/play\r
+     * button is clicked\r
+     *\r
+     * @param event of type {@link MediaOverlayPlayPauseEvent} contains if paused/played\r
+     */\r
+    @SuppressWarnings("unused")\r
+    @Subscribe\r
+    public void pauseButtonClicked(MediaOverlayPlayPauseEvent event) {\r
+        if (isAdded()\r
+                && spineItem.href.equals(event.getHref())) {\r
+            mediaController.stateChanged(event);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * [EVENT BUS FUNCTION]\r
+     * Function triggered from {@link FolioActivity#initAudioView()} when speed\r
+     * change buttons are clicked\r
+     *\r
+     * @param event of type {@link MediaOverlaySpeedEvent} contains selected speed\r
+     *              type HALF,ONE,ONE_HALF and TWO.\r
+     */\r
+    @SuppressWarnings("unused")\r
+    @Subscribe\r
+    public void speedChanged(MediaOverlaySpeedEvent event) {\r
+        mediaController.setSpeed(event.getSpeed());\r
+    }\r
+\r
+    /**\r
+     * [EVENT BUS FUNCTION]\r
+     * Function triggered from {@link FolioActivity#initAudioView()} when new\r
+     * style is selected on button click.\r
+     *\r
+     * @param event of type {@link MediaOverlaySpeedEvent} contains selected style\r
+     *              of type DEFAULT,UNDERLINE and BACKGROUND.\r
+     */\r
+    @SuppressWarnings("unused")\r
+    @Subscribe\r
+    public void styleChanged(MediaOverlayHighlightStyleEvent event) {\r
+        if (isAdded()) {\r
+            switch (event.getStyle()) {\r
+                case DEFAULT:\r
+                    highlightStyle =\r
+                            HighlightImpl.HighlightStyle.classForStyle(HighlightImpl.HighlightStyle.Normal);\r
+                    break;\r
+                case UNDERLINE:\r
+                    highlightStyle =\r
+                            HighlightImpl.HighlightStyle.classForStyle(HighlightImpl.HighlightStyle.DottetUnderline);\r
+                    break;\r
+                case BACKGROUND:\r
+                    highlightStyle =\r
+                            HighlightImpl.HighlightStyle.classForStyle(HighlightImpl.HighlightStyle.TextColor);\r
+                    break;\r
+            }\r
+            mWebview.loadUrl(String.format(getString(R.string.setmediaoverlaystyle), highlightStyle));\r
+        }\r
+    }\r
+\r
+    /**\r
+     * [EVENT BUS FUNCTION]\r
+     * Function triggered when any EBook configuration is changed.\r
+     *\r
+     * @param reloadDataEvent empty POJO.\r
+     */\r
+    @Subscribe\r
+    public void reload(ReloadDataEvent reloadDataEvent) {\r
+        if (isAdded()) {\r
+            mLastWebviewScrollpos = mWebview.getScrollY();\r
+            mIsPageReloaded = true;\r
+            setHtml(true);\r
+            updatePagesLeftTextBg();\r
+        }\r
+    }\r
+\r
+    /**\r
+     * [EVENT BUS FUNCTION]\r
+     * Function triggered from {@link FolioActivity#onActivityResult(int, int, Intent)} when any item in toc clicked.\r
+     *\r
+     * @param event of type {@link AnchorIdEvent} contains selected chapter href.\r
+     */\r
+    @Subscribe\r
+    public void jumpToAnchorPoint(AnchorIdEvent event) {\r
+        if (isAdded() && event != null && event.getHref() != null) {\r
+            String href = event.getHref();\r
+            if (href != null && href.indexOf('#') != -1 && spineItem.href.equals(href.substring(0, href.lastIndexOf('#')))) {\r
+                mAnchorId = href.substring(href.lastIndexOf('#') + 1);\r
+                if (mWebview.getContentHeight() > 0 && mAnchorId != null) {\r
+                    mWebview.loadUrl("javascript:document.getElementById(\"" + mAnchorId + "\").scrollIntoView()");\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public void onReceiveHtml(String html) {\r
+        if (isAdded()) {\r
+            mHtmlString = html;\r
+            setHtml(false);\r
+        }\r
+    }\r
+\r
+    private void setHtml(boolean reloaded) {\r
+        if (spineItem != null) {\r
+            String ref = spineItem.href;\r
+            if (!reloaded && spineItem.properties.contains("media-overlay")) {\r
+                mediaController.setSMILItems(SMILParser.parseSMIL(mHtmlString));\r
+                mediaController.setUpMediaPlayer(spineItem.mediaOverlay, spineItem.mediaOverlay.getAudioPath(spineItem.href), mBookTitle);\r
+            }\r
+            mConfig = AppUtil.getSavedConfig(getActivity());\r
+            String path = ref.substring(0, ref.lastIndexOf('/'));\r
+            mWebview.loadDataWithBaseURL(\r
+                    Constants.LOCALHOST + mBookTitle + "/" + path + "/",\r
+                    HtmlUtil.getHtmlContent(getActivity(), mHtmlString, mConfig),\r
+                    "text/html",\r
+                    "UTF-8",\r
+                    null);\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public void onStop() {\r
+        super.onStop();\r
+        mediaController.stop();\r
+        //TODO save last media overlay item\r
+    }\r
+\r
+    private void initWebView() {\r
+        mWebview = (ObservableWebView) mRootView.findViewById(R.id.contentWebView);\r
+        mWebview.setSeekBarListener(FolioPageFragment.this);\r
+\r
+        if (getActivity() instanceof ObservableWebView.ToolBarListener)\r
+            mWebview.setToolBarListener((ObservableWebView.ToolBarListener) getActivity());\r
+\r
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\r
+            WebView.setWebContentsDebuggingEnabled(true);\r
+        }\r
+\r
+        setupScrollBar();\r
+        mWebview.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {\r
+            @Override\r
+            public void onLayoutChange(View view, int left, int top, int right, int bottom,\r
+                                       int oldLeft, int oldTop, int oldRight, int oldBottom) {\r
+                int height =\r
+                        (int) Math.floor(mWebview.getContentHeight() * mWebview.getScale());\r
+                int webViewHeight = mWebview.getMeasuredHeight();\r
+                mScrollSeekbar.setMaximum(height - webViewHeight);\r
+            }\r
+        });\r
+\r
+        mWebview.getSettings().setJavaScriptEnabled(true);\r
+        mWebview.setVerticalScrollBarEnabled(false);\r
+        mWebview.getSettings().setAllowFileAccess(true);\r
+\r
+        mWebview.setHorizontalScrollBarEnabled(false);\r
+\r
+        mWebview.addJavascriptInterface(this, "Highlight");\r
+        mWebview.setScrollListener(new ObservableWebView.ScrollListener() {\r
+            @Override\r
+            public void onScrollChange(int percent) {\r
+                if (mWebview.getScrollY() != 0) {\r
+                    mScrollY = mWebview.getScrollY();\r
+                    if (isAdded()) {\r
+                        ((FolioActivity) getActivity()).setLastWebViewPosition(mScrollY);\r
+                    }\r
+                }\r
+                mScrollSeekbar.setProgressAndThumb(percent);\r
+                updatePagesLeftText(percent);\r
+\r
+            }\r
+        });\r
+\r
+        mWebview.setWebViewClient(new WebViewClient() {\r
+            @Override\r
+            public void onPageFinished(WebView view, String url) {\r
+                if (isAdded()) {\r
+                    if (mAnchorId != null)\r
+                        view.loadUrl("javascript:document.getElementById(\"" + mAnchorId + "\").scrollIntoView()");\r
+                    view.loadUrl("javascript:alert(getReadingTime())");\r
+                    if (!hasMediaOverlay) {\r
+                        view.loadUrl("javascript:alert(wrappingSentencesWithinPTags())");\r
+                    }\r
+                    view.loadUrl(String.format(getString(R.string.setmediaoverlaystyle),\r
+                            HighlightImpl.HighlightStyle.classForStyle(\r
+                                    HighlightImpl.HighlightStyle.Normal)));\r
+                    if (isCurrentFragment()) {\r
+                        setWebViewPosition(AppUtil.getPreviousBookStateWebViewPosition(getActivity(), mBookTitle));\r
+                    } else if (mIsPageReloaded) {\r
+                        setWebViewPosition(mLastWebviewScrollpos);\r
+                        mIsPageReloaded = false;\r
+                    }\r
+                    String rangy = HighlightUtil.generateRangyString(getPageName());\r
+                    FolioPageFragment.this.rangy = rangy;\r
+                    if (!rangy.isEmpty()) {\r
+                        loadRangy(view, rangy);\r
+                    }\r
+\r
+                    scrollToHighlightId();\r
+\r
+\r
+                }\r
+            }\r
+\r
+            @Override\r
+            public boolean shouldOverrideUrlLoading(WebView view, String url) {\r
+                if (!url.isEmpty() && url.length() > 0) {\r
+                    if (Uri.parse(url).getScheme().startsWith("highlight")) {\r
+                        final Pattern pattern = Pattern.compile(getString(R.string.pattern));\r
+                        try {\r
+                            String htmlDecode = URLDecoder.decode(url, "UTF-8");\r
+                            Matcher matcher = pattern.matcher(htmlDecode.substring(12));\r
+                            if (matcher.matches()) {\r
+                                double left = Double.parseDouble(matcher.group(1));\r
+                                double top = Double.parseDouble(matcher.group(2));\r
+                                double width = Double.parseDouble(matcher.group(3));\r
+                                double height = Double.parseDouble(matcher.group(4));\r
+                                onHighlight((int) (UiUtil.convertDpToPixel((float) left,\r
+                                        getActivity())),\r
+                                        (int) (UiUtil.convertDpToPixel((float) top,\r
+                                                getActivity())),\r
+                                        (int) (UiUtil.convertDpToPixel((float) width,\r
+                                                getActivity())),\r
+                                        (int) (UiUtil.convertDpToPixel((float) height,\r
+                                                getActivity())));\r
+                            }\r
+                        } catch (UnsupportedEncodingException e) {\r
+                            Log.d(TAG, e.getMessage());\r
+                        }\r
+                    } else {\r
+                        if (url.contains("storage")) {\r
+                            mActivityCallback.setPagerToPosition(url);\r
+                        } else if (url.endsWith(".xhtml") || url.endsWith(".html") || url.contains(".xhtml#") || url.contains(".html#")) {\r
+                            mActivityCallback.goToChapter(url);\r
+                        } else {\r
+                            // Otherwise, give the default behavior (open in browser)\r
+                            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));\r
+                            startActivity(intent);\r
+                        }\r
+                    }\r
+                }\r
+                return true;\r
+            }\r
+\r
+\r
+            // prevent favicon.ico to be loaded automatically\r
+            @Override\r
+            public WebResourceResponse shouldInterceptRequest(WebView view, String url) {\r
+                if(url.toLowerCase().contains("/favicon.ico")) {\r
+                    try {\r
+                        return new WebResourceResponse("image/png", null, null);\r
+                    } catch (Exception e) {\r
+                        Log.e(TAG, "shouldInterceptRequest failed", e);\r
+                    }\r
+                }\r
+                return null;\r
+            }\r
+\r
+            // prevent favicon.ico to be loaded automatically\r
+            @Override\r
+            @SuppressLint("NewApi")\r
+            public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {\r
+                if(!request.isForMainFrame() && request.getUrl().getPath().endsWith("/favicon.ico")) {\r
+                    try {\r
+                        return new WebResourceResponse("image/png", null, null);\r
+                    } catch (Exception e) {\r
+                        Log.e(TAG, "shouldInterceptRequest failed", e);\r
+                    }\r
+                }\r
+                return null;\r
+            }\r
+        });\r
+\r
+        mWebview.setWebChromeClient(new WebChromeClient() {\r
+            @Override\r
+            public void onProgressChanged(WebView view, int progress) {\r
+\r
+                if (view.getProgress() == 100) {\r
+                    mWebview.postDelayed(new Runnable() {\r
+                        @Override\r
+                        public void run() {\r
+                            Log.d("scroll y", "Scrolly" + mScrollY);\r
+                            mWebview.scrollTo(0, mScrollY);\r
+                        }\r
+                    }, 100);\r
+                }\r
+            }\r
+\r
+            @Override\r
+            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {\r
+                if (FolioPageFragment.this.isVisible()) {\r
+                    String rangyPattern = "\\d+\\$\\d+\\$\\d+\\$\\w+\\$";\r
+                    Pattern pattern = Pattern.compile(rangyPattern);\r
+                    Matcher matcher = pattern.matcher(message);\r
+                    if (matcher.matches()) {\r
+                        HighlightImpl highlightImpl = HighLightTable.getHighlightForRangy(message);\r
+                        if (HighLightTable.deleteHighlight(message)) {\r
+                            String rangy = HighlightUtil.generateRangyString(getPageName());\r
+                            loadRangy(view, rangy);\r
+                           // mTextSelectionSupport.endSelectionMode();\r
+                            if (highlightImpl != null) {\r
+                                HighlightUtil.sendHighlightBroadcastEvent(\r
+                                        FolioPageFragment.this.getActivity().getApplicationContext(),\r
+                                        highlightImpl,\r
+                                        HighLight.HighLightAction.DELETE);\r
+                            }\r
+                        }\r
+                    } else if (TextUtils.isDigitsOnly(message)) {\r
+                        mTotalMinutes = Integer.parseInt(message);\r
+                    } else {\r
+                        pattern = Pattern.compile(getString(R.string.pattern));\r
+                        matcher = pattern.matcher(message);\r
+                        if (matcher.matches()) {\r
+                            double left = Double.parseDouble(matcher.group(1));\r
+                            double top = Double.parseDouble(matcher.group(2));\r
+                            double width = Double.parseDouble(matcher.group(3));\r
+                            double height = Double.parseDouble(matcher.group(4));\r
+                            showTextSelectionMenu((int) (UiUtil.convertDpToPixel((float) left,\r
+                                    getActivity())),\r
+                                    (int) (UiUtil.convertDpToPixel((float) top,\r
+                                            getActivity())),\r
+                                    (int) (UiUtil.convertDpToPixel((float) width,\r
+                                            getActivity())),\r
+                                    (int) (UiUtil.convertDpToPixel((float) height,\r
+                                            getActivity())));\r
+                        } else {\r
+                            // to handle TTS playback when highlight is deleted.\r
+                            Pattern p = Pattern.compile("[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}");\r
+                            if (!p.matcher(message).matches() && (!message.equals("undefined")) && isCurrentFragment()) {\r
+                                mediaController.speakAudio(message);\r
+                            }\r
+                        }\r
+                    }\r
+                    result.confirm();\r
+                }\r
+                return true;\r
+            }\r
+        });\r
+\r
+//        mTextSelectionSupport = TextSelectionSupport.support(getActivity(), mWebview);\r
+//        mTextSelectionSupport.setSelectionListener(new TextSelectionSupport.SelectionListener() {\r
+//            @Override\r
+//            public void startSelection() {\r
+//            }\r
+//\r
+//            @Override\r
+//            public void selectionChanged(String text) {\r
+//                mSelectedText = text;\r
+//                getActivity().runOnUiThread(new Runnable() {\r
+//                    @Override\r
+//                    public void run() {\r
+//                        mWebview.loadUrl("javascript:alert(getRectForSelectedText())");\r
+//                    }\r
+//                });\r
+//            }\r
+//\r
+//            @Override\r
+//            public void endSelection() {\r
+//\r
+//            }\r
+//        });\r
+\r
+        mWebview.getSettings().setDefaultTextEncodingName("utf-8");\r
+        mWebview.setOnLongClickListener(new View.OnLongClickListener() {\r
+            @Override\r
+            public boolean onLongClick(View v) {\r
+                return true;\r
+            }\r
+        });\r
+        mWebview.setLongClickable(false);\r
+        mWebview.setHapticFeedbackEnabled(false);\r
+        new HtmlTask(this).execute(getWebviewUrl());\r
+    }\r
+\r
+    private void loadRangy(WebView view, String rangy) {\r
+        view.loadUrl(String.format("javascript:if(typeof ssReader !== \"undefined\"){ssReader.setHighlights('%s');}", rangy));\r
+    }\r
+\r
+    private void setupScrollBar() {\r
+        UiUtil.setColorToImage(getActivity(), mConfig.getThemeColor(), mScrollSeekbar.getProgressDrawable());\r
+        Drawable thumbDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.icons_sroll);\r
+        UiUtil.setColorToImage(getActivity(), mConfig.getThemeColor(), (thumbDrawable));\r
+        mScrollSeekbar.setThumb(thumbDrawable);\r
+    }\r
+\r
+    private void initSeekbar() {\r
+        mScrollSeekbar = (VerticalSeekbar) mRootView.findViewById(R.id.scrollSeekbar);\r
+        mScrollSeekbar.getProgressDrawable()\r
+                .setColorFilter(getResources()\r
+                                .getColor(R.color.app_green),\r
+                        PorterDuff.Mode.SRC_IN);\r
+    }\r
+\r
+    private void updatePagesLeftTextBg() {\r
+\r
+        if (mConfig.isNightMode()) {\r
+            mRootView.findViewById(R.id.indicatorLayout)\r
+                    .setBackgroundColor(getResources().getColor(R.color.dark_night));\r
+        } else {\r
+            mRootView.findViewById(R.id.indicatorLayout)\r
+                    .setBackgroundColor(Color.WHITE);\r
+        }\r
+    }\r
+\r
+    private void updatePagesLeftText(int scrollY) {\r
+        try {\r
+            int currentPage = (int) (Math.ceil((double) scrollY / mWebview.getWebViewHeight()) + 1);\r
+            int totalPages =\r
+                    (int) Math.ceil((double) mWebview.getContentHeightVal()\r
+                            / mWebview.getWebViewHeight());\r
+            int pagesRemaining = totalPages - currentPage;\r
+            String pagesRemainingStrFormat =\r
+                    pagesRemaining > 1 ?\r
+                            getString(R.string.pages_left) : getString(R.string.page_left);\r
+            String pagesRemainingStr = String.format(Locale.US,\r
+                    pagesRemainingStrFormat, pagesRemaining);\r
+\r
+            int minutesRemaining =\r
+                    (int) Math.ceil((double) (pagesRemaining * mTotalMinutes) / totalPages);\r
+            String minutesRemainingStr;\r
+            if (minutesRemaining > 1) {\r
+                minutesRemainingStr =\r
+                        String.format(Locale.US, getString(R.string.minutes_left),\r
+                                minutesRemaining);\r
+            } else if (minutesRemaining == 1) {\r
+                minutesRemainingStr =\r
+                        String.format(Locale.US, getString(R.string.minute_left),\r
+                                minutesRemaining);\r
+            } else {\r
+                minutesRemainingStr = getString(R.string.less_than_minute);\r
+            }\r
+\r
+            mMinutesLeftTextView.setText(minutesRemainingStr);\r
+            mPagesLeftTextView.setText(pagesRemainingStr);\r
+        } catch (java.lang.ArithmeticException exp) {\r
+            Log.d("divide error", exp.toString());\r
+        }\r
+    }\r
+\r
+    private void initAnimations() {\r
+        mFadeInAnimation = AnimationUtils.loadAnimation(getActivity(), R.anim.fadein);\r
+        mFadeInAnimation.setAnimationListener(new Animation.AnimationListener() {\r
+            @Override\r
+            public void onAnimationStart(Animation animation) {\r
+                mScrollSeekbar.setVisibility(View.VISIBLE);\r
+            }\r
+\r
+            @Override\r
+            public void onAnimationEnd(Animation animation) {\r
+                fadeOutSeekBarIfVisible();\r
+            }\r
+\r
+            @Override\r
+            public void onAnimationRepeat(Animation animation) {\r
+\r
+            }\r
+        });\r
+        mFadeOutAnimation = AnimationUtils.loadAnimation(getActivity(), R.anim.fadeout);\r
+        mFadeOutAnimation.setAnimationListener(new Animation.AnimationListener() {\r
+            @Override\r
+            public void onAnimationStart(Animation animation) {\r
+\r
+            }\r
+\r
+            @Override\r
+            public void onAnimationEnd(Animation animation) {\r
+                mScrollSeekbar.setVisibility(View.INVISIBLE);\r
+            }\r
+\r
+            @Override\r
+            public void onAnimationRepeat(Animation animation) {\r
+\r
+            }\r
+        });\r
+    }\r
+\r
+    public void fadeInSeekBarIfInvisible() {\r
+        if (mScrollSeekbar.getVisibility() == View.INVISIBLE ||\r
+                mScrollSeekbar.getVisibility() == View.GONE) {\r
+            mScrollSeekbar.startAnimation(mFadeInAnimation);\r
+        }\r
+    }\r
+\r
+    public void fadeOutSeekBarIfVisible() {\r
+        if (mScrollSeekbar.getVisibility() == View.VISIBLE) {\r
+            mScrollSeekbar.startAnimation(mFadeOutAnimation);\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public void onDestroyView() {\r
+        mFadeInAnimation.setAnimationListener(null);\r
+        mFadeOutAnimation.setAnimationListener(null);\r
+\r
+        Activity activity = getActivity();\r
+        if (activity instanceof BusOwner)\r
+            ((BusOwner) activity).getBus().unregister(this);\r
+        super.onDestroyView();\r
+    }\r
+\r
+    @Override\r
+    public void onSaveInstanceState(Bundle outState) {\r
+        super.onSaveInstanceState(outState);\r
+        outState.putInt(KEY_FRAGMENT_FOLIO_POSITION, mPosition);\r
+        outState.putString(KEY_FRAGMENT_FOLIO_BOOK_TITLE, mBookTitle);\r
+        outState.putString(KEY_FRAGMENT_EPUB_FILE_NAME, mEpubFileName);\r
+        outState.putSerializable(SPINE_ITEM, spineItem);\r
+    }\r
+\r
+    public void highlight(HighlightImpl.HighlightStyle style, boolean isCreated) {\r
+        if (isCreated) {\r
+            mWebview.loadUrl(String.format("javascript:if(typeof ssReader !== \"undefined\"){ssReader.highlightSelection('%s');}", HighlightImpl.HighlightStyle.classForStyle(style)));\r
+        } else {\r
+            mWebview.loadUrl(String.format("javascript:alert(setHighlightStyle('%s'))", "highlight_" + HighlightImpl.HighlightStyle.classForStyle(style)));\r
+        }\r
+    }\r
+\r
+    public void highlightRemove() {\r
+        mWebview.loadUrl("javascript:alert(removeThisHighlight())");\r
+    }\r
+\r
+    public void showTextSelectionMenu(int x, int y, final int width, final int height) {\r
+        final ViewGroup root =\r
+                (ViewGroup) getActivity().getWindow()\r
+                        .getDecorView().findViewById(android.R.id.content);\r
+        final View view = new View(getActivity());\r
+        view.setLayoutParams(new ViewGroup.LayoutParams(width, height));\r
+        view.setBackgroundColor(Color.TRANSPARENT);\r
+\r
+        root.addView(view);\r
+\r
+        view.setX(x);\r
+        view.setY(y);\r
+        final QuickAction quickAction =\r
+                new QuickAction(getActivity(), QuickAction.HORIZONTAL);\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_COPY,\r
+                getString(R.string.copy)));\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_HIGHLIGHT,\r
+                getString(R.string.highlight)));\r
+        if (!mSelectedText.trim().contains(" ")) {\r
+            quickAction.addActionItem(new ActionItem(ACTION_ID_DEFINE,\r
+                    getString(R.string.define)));\r
+        }\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_SHARE,\r
+                getString(R.string.share)));\r
+        quickAction.setOnActionItemClickListener(new QuickAction.OnActionItemClickListener() {\r
+            @Override\r
+            public void onItemClick(QuickAction source, int pos, int actionId) {\r
+                quickAction.dismiss();\r
+                root.removeView(view);\r
+                onTextSelectionActionItemClicked(actionId, view, width, height);\r
+            }\r
+        });\r
+        quickAction.show(view, width, height);\r
+    }\r
+\r
+    private void onTextSelectionActionItemClicked(int actionId, View view, int width, int height) {\r
+        if (actionId == ACTION_ID_COPY) {\r
+            UiUtil.copyToClipboard(getActivity(), mSelectedText);\r
+            Toast.makeText(getActivity(), getString(R.string.copied), Toast.LENGTH_SHORT).show();\r
+            mTextSelectionSupport.endSelectionMode();\r
+        } else if (actionId == ACTION_ID_SHARE) {\r
+            UiUtil.share(getActivity(), mSelectedText);\r
+        } else if (actionId == ACTION_ID_DEFINE) {\r
+            showDictDialog(mSelectedText);\r
+            mTextSelectionSupport.endSelectionMode();\r
+        } else if (actionId == ACTION_ID_HIGHLIGHT) {\r
+            onHighlight(view, width, height, true);\r
+        }\r
+    }\r
+\r
+    private void showDictDialog(String mSelectedText) {\r
+        DictionaryFragment dictionaryFragment = new DictionaryFragment();\r
+        Bundle b = new Bundle();\r
+        b.putString(Constants.SELECTED_WORD, mSelectedText);\r
+        dictionaryFragment.setArguments(b);\r
+        dictionaryFragment.show(getFragmentManager(), DictionaryFragment.class.getName());\r
+    }\r
+\r
+    private void onHighlight(int x, int y, int width, int height) {\r
+        final View view = new View(getActivity());\r
+        view.setLayoutParams(new ViewGroup.LayoutParams(width, height));\r
+        view.setBackgroundColor(Color.TRANSPARENT);\r
+        view.setX(x);\r
+        view.setY(y);\r
+        onHighlight(view, width, height, false);\r
+    }\r
+\r
+    private void onHighlight(final View view, int width, int height, final boolean isCreated) {\r
+        ViewGroup root =\r
+                (ViewGroup) getActivity().getWindow().\r
+                        getDecorView().findViewById(android.R.id.content);\r
+        ViewGroup parent = (ViewGroup) view.getParent();\r
+        if (parent == null) {\r
+            root.addView(view);\r
+        } else {\r
+            final int index = parent.indexOfChild(view);\r
+            parent.removeView(view);\r
+            parent.addView(view, index);\r
+        }\r
+\r
+        final QuickAction quickAction = new QuickAction(getActivity(), QuickAction.HORIZONTAL);\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_HIGHLIGHT_COLOR,\r
+                getResources().getDrawable(R.drawable.colors_marker)));\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_DELETE,\r
+                getResources().getDrawable(R.drawable.ic_action_discard)));\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_SHARE,\r
+                getResources().getDrawable(R.drawable.ic_action_share)));\r
+        final ViewGroup finalRoot = root;\r
+        quickAction.setOnActionItemClickListener(new QuickAction.OnActionItemClickListener() {\r
+            @Override\r
+            public void onItemClick(QuickAction source, int pos, int actionId) {\r
+                quickAction.dismiss();\r
+                finalRoot.removeView(view);\r
+                onHighlightActionItemClicked(actionId, view, isCreated);\r
+            }\r
+        });\r
+        quickAction.show(view, width, height);\r
+    }\r
+\r
+    private void onHighlightActionItemClicked(int actionId, View view, boolean isCreated) {\r
+        if (actionId == ACTION_ID_HIGHLIGHT_COLOR) {\r
+            onHighlightColors(view, isCreated);\r
+        } else if (actionId == ACTION_ID_SHARE) {\r
+            UiUtil.share(getActivity(), mSelectedText);\r
+            mTextSelectionSupport.endSelectionMode();\r
+        } else if (actionId == ACTION_ID_DELETE) {\r
+            highlightRemove();\r
+        }\r
+    }\r
+\r
+    private void onHighlightColors(final View view, final boolean isCreated) {\r
+        ViewGroup root =\r
+                (ViewGroup) getActivity().getWindow()\r
+                        .getDecorView().findViewById(android.R.id.content);\r
+        ViewGroup parent = (ViewGroup) view.getParent();\r
+        if (parent == null) {\r
+            root.addView(view);\r
+        } else {\r
+            final int index = parent.indexOfChild(view);\r
+            parent.removeView(view);\r
+            parent.addView(view, index);\r
+        }\r
+\r
+        final QuickAction quickAction = new QuickAction(getActivity(), QuickAction.HORIZONTAL);\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_HIGHLIGHT_YELLOW,\r
+                getResources().getDrawable(R.drawable.ic_yellow_marker)));\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_HIGHLIGHT_GREEN,\r
+                getResources().getDrawable(R.drawable.ic_green_marker)));\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_HIGHLIGHT_BLUE,\r
+                getResources().getDrawable(R.drawable.ic_blue_marker)));\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_HIGHLIGHT_PINK,\r
+                getResources().getDrawable(R.drawable.ic_pink_marker)));\r
+        quickAction.addActionItem(new ActionItem(ACTION_ID_HIGHLIGHT_UNDERLINE,\r
+                getResources().getDrawable(R.drawable.ic_underline_marker)));\r
+        final ViewGroup finalRoot = root;\r
+        quickAction.setOnActionItemClickListener(new QuickAction.OnActionItemClickListener() {\r
+            @Override\r
+            public void onItemClick(QuickAction source, int pos, int actionId) {\r
+                quickAction.dismiss();\r
+                finalRoot.removeView(view);\r
+                onHighlightColorsActionItemClicked(actionId, view, isCreated);\r
+            }\r
+        });\r
+        quickAction.show(view);\r
+    }\r
+\r
+    private void onHighlightColorsActionItemClicked(int actionId, View view, boolean isCreated) {\r
+        if (actionId == ACTION_ID_HIGHLIGHT_YELLOW) {\r
+            highlight(HighlightImpl.HighlightStyle.Yellow, isCreated);\r
+        } else if (actionId == ACTION_ID_HIGHLIGHT_GREEN) {\r
+            highlight(HighlightImpl.HighlightStyle.Green, isCreated);\r
+        } else if (actionId == ACTION_ID_HIGHLIGHT_BLUE) {\r
+            highlight(HighlightImpl.HighlightStyle.Blue, isCreated);\r
+        } else if (actionId == ACTION_ID_HIGHLIGHT_PINK) {\r
+            highlight(HighlightImpl.HighlightStyle.Pink, isCreated);\r
+        } else if (actionId == ACTION_ID_HIGHLIGHT_UNDERLINE) {\r
+            highlight(HighlightImpl.HighlightStyle.Underline, isCreated);\r
+        }\r
+        mTextSelectionSupport.endSelectionMode();\r
+    }\r
+\r
+    @Override\r
+    public void resetCurrentIndex() {\r
+        if (isCurrentFragment()) {\r
+            mWebview.loadUrl("javascript:alert(rewindCurrentIndex())");\r
+        }\r
+    }\r
+\r
+    @SuppressWarnings("unused")\r
+    @JavascriptInterface\r
+    public void onReceiveHighlights(String html) {\r
+        if (html != null) {\r
+            rangy = HighlightUtil.createHighlightRangy(getActivity().getApplicationContext(),\r
+                    html,\r
+                    mBookId,\r
+                    getPageName(),\r
+                    mPosition,\r
+                    rangy);\r
+        }\r
+    }\r
+\r
+    private String getPageName() {\r
+        return mBookTitle + "$" + spineItem.href;\r
+    }\r
+\r
+    @SuppressWarnings("unused")\r
+    @Subscribe\r
+    public void setWebView(final WebViewPosition position) {\r
+        if (position.getHref().equals(spineItem.href) && isAdded()) {\r
+            highlightId = position.getHighlightId();\r
+\r
+            if (mWebview.getContentHeight() > 0) {\r
+                scrollToHighlightId();\r
+                //Webview.loadUrl(String.format(getString(R.string.goto_highlight), highlightId));\r
+            }\r
+        }\r
+    }\r
+\r
+    public void setWebViewPosition(final int position) {\r
+        mWebview.post(new Runnable() {\r
+            @Override\r
+            public void run() {\r
+                if (isAdded()) {\r
+                    mWebview.scrollTo(0, position);\r
+                }\r
+            }\r
+        });\r
+    }\r
+\r
+    @Override\r
+    public void highLightText(String fragmentId) {\r
+        mWebview.loadUrl(String.format(getString(R.string.audio_mark_id), fragmentId));\r
+    }\r
+\r
+    @Override\r
+    public void highLightTTS() {\r
+        mWebview.loadUrl("javascript:alert(getSentenceWithIndex('epub-media-overlay-playing'))");\r
+    }\r
+\r
+    @JavascriptInterface\r
+    public void getUpdatedHighlightId(String id, String style) {\r
+        if (id != null) {\r
+            HighlightImpl highlightImpl = HighLightTable.updateHighlightStyle(id, style);\r
+            if (highlightImpl != null) {\r
+                HighlightUtil.sendHighlightBroadcastEvent(\r
+                        getActivity().getApplicationContext(),\r
+                        highlightImpl,\r
+                        HighLight.HighLightAction.MODIFY);\r
+            }\r
+            final String rangyString = HighlightUtil.generateRangyString(getPageName());\r
+            getActivity().runOnUiThread(new Runnable() {\r
+                public void run() {\r
+                    loadRangy(mWebview, rangyString);\r
+                }\r
+            });\r
+\r
+        }\r
+    }\r
+\r
+    @Subscribe\r
+    public void resetCurrentIndex(RewindIndexEvent resetIndex) {\r
+        if (isCurrentFragment()) {\r
+            mWebview.loadUrl("javascript:alert(rewindCurrentIndex())");\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public void onDestroy() {\r
+        super.onDestroy();\r
+        if (mWebview != null) mWebview.destroy();\r
+    }\r
+\r
+    private boolean isCurrentFragment() {\r
+        return isAdded() && ((FolioActivity) getActivity()).getmChapterPosition() == mPos;\r
+    }\r
+\r
+    public void setFragmentPos(int pos) {\r
+        mPos = pos;\r
+    }\r
+\r
+    @Override\r
+    public void onError() {\r
+    }\r
+\r
+    private void scrollToHighlightId() {\r
+        mWebview.loadUrl(String.format(getString(R.string.goto_highlight), highlightId));\r
+    }\r
+}\r
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/fragment/HighlightFragment.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/fragment/HighlightFragment.java
new file mode 100755 (executable)
index 0000000..b835482
--- /dev/null
@@ -0,0 +1,134 @@
+package com.folioreader.ui.folio.fragment;\r
+\r
+import android.app.Activity;\r
+import android.app.Dialog;\r
+import android.content.Intent;\r
+import android.os.Bundle;\r
+import android.support.annotation.Nullable;\r
+import android.support.v4.app.Fragment;\r
+import android.support.v4.content.ContextCompat;\r
+import android.support.v7.widget.DividerItemDecoration;\r
+import android.support.v7.widget.LinearLayoutManager;\r
+import android.support.v7.widget.RecyclerView;\r
+import android.text.TextUtils;\r
+import android.view.LayoutInflater;\r
+import android.view.View;\r
+import android.view.ViewGroup;\r
+import android.view.Window;\r
+import android.widget.EditText;\r
+import android.widget.Toast;\r
+\r
+import com.folioreader.Config;\r
+import com.folioreader.Constants;\r
+import com.folioreader.R;\r
+import com.folioreader.model.HighLight;\r
+import com.folioreader.model.HighlightImpl;\r
+import com.folioreader.model.event.BusOwner;\r
+import com.folioreader.model.event.ReloadDataEvent;\r
+import com.folioreader.model.sqlite.HighLightTable;\r
+import com.folioreader.ui.folio.adapter.HighlightAdapter;\r
+import com.folioreader.util.AppUtil;\r
+import com.folioreader.util.FolioReader;\r
+import com.folioreader.util.HighlightUtil;\r
+\r
+public class HighlightFragment extends Fragment implements HighlightAdapter.HighLightAdapterCallback {\r
+    private static final String HIGHLIGHT_ITEM = "highlight_item";\r
+    private View mRootView;\r
+    private HighlightAdapter adapter;\r
+    private String mBookId;\r
+\r
+\r
+    public static HighlightFragment newInstance(String bookId, String epubTitle) {\r
+        HighlightFragment highlightFragment = new HighlightFragment();\r
+        Bundle args = new Bundle();\r
+        args.putString(FolioReader.INTENT_BOOK_ID, bookId);\r
+        args.putString(Constants.BOOK_TITLE, epubTitle);\r
+        highlightFragment.setArguments(args);\r
+        return highlightFragment;\r
+    }\r
+\r
+    @Override\r
+    public void onCreate(@Nullable Bundle savedInstanceState) {\r
+        super.onCreate(savedInstanceState);\r
+    }\r
+\r
+    @Override\r
+    public View onCreateView(LayoutInflater inflater,\r
+                             ViewGroup container, Bundle savedInstanceState) {\r
+        mRootView = inflater.inflate(R.layout.fragment_highlight_list, container, false);\r
+        return mRootView;\r
+    }\r
+\r
+    @Override\r
+    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {\r
+        super.onViewCreated(view, savedInstanceState);\r
+        RecyclerView highlightsView = (RecyclerView) mRootView.findViewById(R.id.rv_highlights);\r
+        Config config = AppUtil.getSavedConfig(getActivity());\r
+        mBookId = getArguments().getString(FolioReader.INTENT_BOOK_ID);\r
+\r
+        if (config.isNightMode()) {\r
+            mRootView.findViewById(R.id.rv_highlights).\r
+                    setBackgroundColor(ContextCompat.getColor(getActivity(),\r
+                            R.color.black));\r
+        }\r
+        highlightsView.setLayoutManager(new LinearLayoutManager(getActivity()));\r
+        highlightsView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL));\r
+\r
+        adapter = new HighlightAdapter(getActivity(), HighLightTable.getAllHighlights(mBookId), this, config);\r
+        highlightsView.setAdapter(adapter);\r
+    }\r
+\r
+    @Override\r
+    public void onItemClick(HighlightImpl highlightImpl) {\r
+        Intent intent = new Intent();\r
+        intent.putExtra(HIGHLIGHT_ITEM, highlightImpl);\r
+        intent.putExtra(Constants.TYPE, Constants.HIGHLIGHT_SELECTED);\r
+        getActivity().setResult(Activity.RESULT_OK, intent);\r
+        getActivity().finish();\r
+    }\r
+\r
+    @Override\r
+    public void deleteHighlight(int id) {\r
+        HighLightTable.deleteHighlight(id);\r
+\r
+        Activity activity = getActivity();\r
+        if (activity instanceof BusOwner)\r
+            ((BusOwner) activity).getBus().post(new ReloadDataEvent());\r
+    }\r
+\r
+    @Override\r
+    public void editNote(final HighlightImpl highlightImpl, final int position) {\r
+        final Dialog dialog = new Dialog(getActivity(), R.style.DialogCustomTheme);\r
+        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);\r
+        dialog.setContentView(R.layout.dialog_edit_notes);\r
+        dialog.show();\r
+        String noteText = highlightImpl.getNote();\r
+        ((EditText) dialog.findViewById(R.id.edit_note)).setText(noteText);\r
+\r
+        dialog.findViewById(R.id.btn_save_note).setOnClickListener(new View.OnClickListener() {\r
+            @Override\r
+            public void onClick(View v) {\r
+\r
+                String note =\r
+                        ((EditText) dialog.findViewById(R.id.edit_note)).getText().toString();\r
+                if (!TextUtils.isEmpty(note)) {\r
+                    highlightImpl.setNote(note);\r
+                    if (HighLightTable.updateHighlight(highlightImpl)) {\r
+                        HighlightUtil.sendHighlightBroadcastEvent(\r
+                                HighlightFragment.this.getActivity().getApplicationContext(),\r
+                                highlightImpl,\r
+                                HighLight.HighLightAction.MODIFY);\r
+                        adapter.editNote(note, position);\r
+                    }\r
+                    dialog.dismiss();\r
+                } else {\r
+                    Toast.makeText(getActivity(),\r
+                            getString(R.string.please_enter_note),\r
+                            Toast.LENGTH_SHORT).show();\r
+                }\r
+            }\r
+        });\r
+    }\r
+}\r
+\r
+\r
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/mediaoverlay/MediaController.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/mediaoverlay/MediaController.java
new file mode 100755 (executable)
index 0000000..f197530
--- /dev/null
@@ -0,0 +1,243 @@
+package com.folioreader.ui.folio.mediaoverlay;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.os.Build;
+import android.os.Handler;
+import android.speech.tts.TextToSpeech;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+
+import com.folioreader.Constants;
+import com.folioreader.model.event.MediaOverlayPlayPauseEvent;
+import com.folioreader.model.event.MediaOverlaySpeedEvent;
+import com.folioreader.model.media_overlay.OverlayItems;
+import com.folioreader.util.UiUtil;
+
+import org.readium.r2_streamer.model.publication.SMIL.Clip;
+import org.readium.r2_streamer.model.publication.SMIL.MediaOverlays;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * @author gautam chibde on 21/6/17.
+ */
+
+public class MediaController {
+
+    private static final String TAG = MediaController.class.getSimpleName();
+
+    public enum MediaType {
+        TTS, SMIL,
+    }
+
+    private MediaType mediaType;
+    private MediaControllerCallbacks callbacks;
+    private Context context;
+
+    //**********************************//
+    //          MEDIA OVERLAY           //
+    //**********************************//
+    private MediaOverlays mediaOverlays;
+    private List<OverlayItems> mediaItems = new ArrayList<>();
+    private int mediaItemPosition = 0;
+    private MediaPlayer mediaPlayer;
+
+    private Clip currentClip;
+
+    private boolean isMediaPlayerReady;
+    private Handler mediaHandler;
+
+    //*********************************//
+    //              TTS                //
+    //*********************************//
+    private TextToSpeech mTextToSpeech;
+    private boolean mIsSpeaking = false;
+
+    public MediaController(Context context, MediaType mediaType, MediaControllerCallbacks callbacks) {
+        this.mediaType = mediaType;
+        this.callbacks = callbacks;
+        this.context = context;
+    }
+
+    private Runnable mHighlightTask = new Runnable() {
+        @Override
+        public void run() {
+            int currentPosition = mediaPlayer.getCurrentPosition();
+            if (mediaPlayer.getDuration() != currentPosition) {
+                if (mediaItemPosition < mediaItems.size()) {
+                    int end = (int) currentClip.end * 1000;
+                    if (currentPosition > end) {
+                        mediaItemPosition++;
+                        currentClip = mediaOverlays.clip(mediaItems.get(mediaItemPosition).getId());
+                        if (currentClip != null) {
+                            callbacks.highLightText(mediaItems.get(mediaItemPosition).getId());
+                        } else {
+                            mediaItemPosition++;
+                        }
+                    }
+                    mediaHandler.postDelayed(mHighlightTask, 10);
+                } else {
+                    mediaHandler.removeCallbacks(mHighlightTask);
+                }
+            }
+        }
+    };
+
+    public void resetMediaPosition() {
+        mediaItemPosition = 0;
+    }
+
+    public void setSMILItems(List<OverlayItems> overlayItems) {
+        this.mediaItems = overlayItems;
+    }
+
+    public void next() {
+        mediaItemPosition++;
+    }
+
+    public void setTextToSpeech(final Context context) {
+        mTextToSpeech = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
+            @Override
+            public void onInit(int status) {
+                if (status != TextToSpeech.ERROR) {
+                    mTextToSpeech.setLanguage(Locale.getDefault());
+                    mTextToSpeech.setSpeechRate(0.70f);
+                }
+
+                mTextToSpeech.setOnUtteranceCompletedListener(
+                        new TextToSpeech.OnUtteranceCompletedListener() {
+                            @Override
+                            public void onUtteranceCompleted(String utteranceId) {
+                                ((AppCompatActivity) context).runOnUiThread(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        if (mIsSpeaking) {
+                                            callbacks.highLightTTS();
+                                        }
+                                    }
+                                });
+                            }
+                        });
+            }
+        });
+    }
+
+    public void setUpMediaPlayer(MediaOverlays mediaOverlays, String path, String mBookTitle) {
+        this.mediaOverlays = mediaOverlays;
+        mediaHandler = new Handler();
+        try {
+            mediaItemPosition = 0;
+            String uri = Constants.LOCALHOST + mBookTitle + "/" + path;
+            mediaPlayer = new MediaPlayer();
+            mediaPlayer.setDataSource(uri);
+            mediaPlayer.prepare();
+            isMediaPlayerReady = true;
+        } catch (IOException e) {
+            Log.d(TAG, e.getMessage());
+        }
+    }
+
+    public void setSpeed(MediaOverlaySpeedEvent.Speed speed) {
+        switch (speed) {
+            case HALF:
+                setPlaybackSpeed(0.5f);
+                break;
+            case ONE:
+                setPlaybackSpeed(1.0f);
+                break;
+            case ONE_HALF:
+                setPlaybackSpeed(1.5f);
+                break;
+            case TWO:
+                setPlaybackSpeed(2.0f);
+                break;
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.M)
+    private void setPlaybackSpeed(float speed) {
+        if (mediaType == MediaType.SMIL) {
+            if (mediaPlayer != null && mediaPlayer.isPlaying()) {
+                mediaPlayer.setPlaybackParams(mediaPlayer.getPlaybackParams().setSpeed(speed));
+            }
+        } else {
+            mTextToSpeech.setSpeechRate(speed);
+        }
+    }
+
+    public void stateChanged(MediaOverlayPlayPauseEvent event) {
+        if (event.isStateChanged()) {
+            if (mediaPlayer != null) {
+                mediaPlayer.pause();
+            }
+            if (mTextToSpeech != null && mTextToSpeech.isSpeaking()) {
+                mTextToSpeech.stop();
+            }
+        } else {
+            if (event.isPlay()) {
+                UiUtil.keepScreenAwake(true, context);
+            } else {
+                UiUtil.keepScreenAwake(false, context);
+            }
+            if (mediaType == MediaType.SMIL) {
+                playSMIL();
+            } else {
+                if (mTextToSpeech.isSpeaking()) {
+                    mTextToSpeech.stop();
+                    mIsSpeaking = false;
+                    callbacks.resetCurrentIndex();
+                } else {
+                    mIsSpeaking = true;
+                    callbacks.highLightTTS();
+                }
+            }
+        }
+    }
+
+    private void playSMIL() {
+        if (mediaPlayer != null && isMediaPlayerReady) {
+            if (mediaPlayer.isPlaying()) {
+                mediaPlayer.pause();
+            } else {
+                currentClip = mediaOverlays.clip(mediaItems.get(mediaItemPosition).getId());
+                if (currentClip != null) {
+                    mediaPlayer.start();
+                    mediaHandler.post(mHighlightTask);
+                } else {
+                    mediaItemPosition++;
+                    mediaPlayer.start();
+                    mediaHandler.post(mHighlightTask);
+                }
+            }
+        }
+    }
+
+    public void speakAudio(String sentence) {
+        if (mediaType == MediaType.TTS) {
+            HashMap<String, String> params = new HashMap<>();
+            params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "stringId");
+            mTextToSpeech.speak(sentence, TextToSpeech.QUEUE_FLUSH, params);
+        }
+    }
+
+    public void stop() {
+        if (mTextToSpeech != null) {
+            if (mTextToSpeech.isSpeaking()) {
+                mTextToSpeech.stop();
+            }
+            mTextToSpeech.shutdown();
+        }
+        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
+            mediaPlayer.stop();
+            mediaPlayer.release();
+            mediaPlayer = null;
+            mediaHandler.removeCallbacks(mHighlightTask);
+        }
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/mediaoverlay/MediaControllerCallbacks.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/mediaoverlay/MediaControllerCallbacks.java
new file mode 100755 (executable)
index 0000000..1cd68c7
--- /dev/null
@@ -0,0 +1,14 @@
+package com.folioreader.ui.folio.mediaoverlay;
+
+/**
+ * @author gautam chibde on 21/6/17.
+ */
+
+public interface MediaControllerCallbacks {
+
+    void highLightText(String text);
+
+    void highLightTTS();
+
+    void resetCurrentIndex();
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/presenter/MainMvpView.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/presenter/MainMvpView.java
new file mode 100755 (executable)
index 0000000..a4d77a4
--- /dev/null
@@ -0,0 +1,13 @@
+package com.folioreader.ui.folio.presenter;
+
+import com.folioreader.ui.base.BaseMvpView;
+
+import org.readium.r2_streamer.model.publication.EpubPublication;
+
+/**
+ * @author gautam chibde on 8/6/17.
+ */
+
+public interface MainMvpView extends BaseMvpView {
+    void onLoadPublication(EpubPublication publication);
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/folio/presenter/MainPresenter.java b/Android/folioreader/src/main/java/com/folioreader/ui/folio/presenter/MainPresenter.java
new file mode 100755 (executable)
index 0000000..00da2e6
--- /dev/null
@@ -0,0 +1,32 @@
+package com.folioreader.ui.folio.presenter;
+
+import com.folioreader.ui.base.ManifestCallBack;
+import com.folioreader.ui.base.ManifestTask;
+
+import org.readium.r2_streamer.model.publication.EpubPublication;
+
+/**
+ * @author gautam chibde on 8/6/17.
+ */
+
+public class MainPresenter implements ManifestCallBack {
+    private MainMvpView mainMvpView;
+
+    public MainPresenter(MainMvpView mainMvpView) {
+        this.mainMvpView = mainMvpView;
+    }
+
+    public void parseManifest(String url) {
+        new ManifestTask(this).execute(url);
+    }
+
+    @Override
+    public void onReceivePublication(EpubPublication publication) {
+        mainMvpView.onLoadPublication(publication);
+    }
+
+    @Override
+    public void onError() {
+        mainMvpView.onError();
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/adapter/TOCAdapter.java b/Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/adapter/TOCAdapter.java
new file mode 100755 (executable)
index 0000000..cef0421
--- /dev/null
@@ -0,0 +1,161 @@
+package com.folioreader.ui.tableofcontents.adapter;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.folioreader.Config;
+import com.folioreader.R;
+import com.folioreader.model.TOCLinkWrapper;
+import com.folioreader.util.MultiLevelExpIndListAdapter;
+
+import java.util.ArrayList;
+
+/**
+ * Created by mahavir on 3/10/17.
+ */
+
+public class TOCAdapter extends MultiLevelExpIndListAdapter {
+
+    private static final int LEVEL_ONE_PADDING_PIXEL = 15;
+
+    private TOCCallback callback;
+    private final Context mContext;
+    private String selectedHref;
+    private Config mConfig;
+
+    public TOCAdapter(Context context, ArrayList<TOCLinkWrapper> tocLinkWrappers, String selectedHref, Config config) {
+        super(tocLinkWrappers);
+        mContext = context;
+        this.selectedHref = selectedHref;
+        this.mConfig = config;
+    }
+
+    public void setCallback(TOCCallback callback) {
+        this.callback = callback;
+    }
+
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        return new TOCRowViewHolder(LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.row_table_of_contents, parent, false));
+    }
+
+    @Override
+    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+        TOCRowViewHolder viewHolder = (TOCRowViewHolder) holder;
+        TOCLinkWrapper tocLinkWrapper = (TOCLinkWrapper) getItemAt(position);
+
+        if (tocLinkWrapper.getChildren() == null || tocLinkWrapper.getChildren().isEmpty()) {
+            viewHolder.children.setVisibility(View.INVISIBLE);
+        } else {
+            viewHolder.children.setVisibility(View.VISIBLE);
+        }
+        viewHolder.sectionTitle.setText(tocLinkWrapper.getTocLink().bookTitle);
+
+        if(mConfig.isNightMode()) {
+            if (tocLinkWrapper.isGroup()) {
+                viewHolder.children.setImageResource(R.drawable.ic_plus_white_24dp);
+            } else {
+                viewHolder.children.setImageResource(R.drawable.ic_minus_white_24dp);
+            }
+        } else {
+            if (tocLinkWrapper.isGroup()) {
+                viewHolder.children.setImageResource(R.drawable.ic_plus_black_24dp);
+            } else {
+                viewHolder.children.setImageResource(R.drawable.ic_minus_black_24dp);
+            }
+        }
+
+//        int leftPadding = getPaddingPixels(mContext, LEVEL_ONE_PADDING_PIXEL) * (tocLinkWrapper.getIndentation());
+//        viewHolder.view.setPadding(leftPadding, 0, 0, 0);
+
+        // set color to each indentation level
+        if (tocLinkWrapper.getIndentation() == 0) {
+            viewHolder.view.setBackgroundColor(Color.WHITE);
+            viewHolder.sectionTitle.setTextColor(Color.BLACK);
+        } else if (tocLinkWrapper.getIndentation() == 1) {
+            viewHolder.view.setBackgroundColor(Color.parseColor("#f7f7f7"));
+            viewHolder.sectionTitle.setTextColor(Color.BLACK);
+        } else if (tocLinkWrapper.getIndentation() == 2) {
+            viewHolder.view.setBackgroundColor(Color.parseColor("#b3b3b3"));
+            viewHolder.sectionTitle.setTextColor(Color.WHITE);
+        } else if (tocLinkWrapper.getIndentation() == 3) {
+            viewHolder.view.setBackgroundColor(Color.parseColor("#f7f7f7"));
+            viewHolder.sectionTitle.setTextColor(Color.BLACK);
+        }
+
+        if (tocLinkWrapper.getChildren() == null || tocLinkWrapper.getChildren().isEmpty()) {
+            viewHolder.children.setVisibility(View.INVISIBLE);
+        } else {
+            viewHolder.children.setVisibility(View.VISIBLE);
+        }
+
+        if(mConfig.isNightMode()){
+            viewHolder.container.setBackgroundColor(ContextCompat.getColor(mContext,
+                    R.color.dark_night));
+            viewHolder.children.setBackgroundColor(ContextCompat.getColor(mContext,
+                    R.color.dark_night));
+            viewHolder.sectionTitle.setTextColor(ContextCompat.getColor(mContext,
+                    R.color.white));
+        } else {
+            viewHolder.container.setBackgroundColor(ContextCompat.getColor(mContext,
+                    R.color.white));
+            viewHolder.children.setBackgroundColor(ContextCompat.getColor(mContext,
+                    R.color.white));
+            viewHolder.sectionTitle.setTextColor(ContextCompat.getColor(mContext,
+                    R.color.black));
+        }
+        if (tocLinkWrapper.getTocLink().href.equals(selectedHref)) {
+            viewHolder.sectionTitle.setTextColor(ContextCompat.getColor(mContext, mConfig.getThemeColor()));
+        }
+    }
+
+    public interface TOCCallback {
+        void onTocClicked(int position);
+
+        void onExpanded(int position);
+    }
+
+    public class TOCRowViewHolder extends RecyclerView.ViewHolder {
+        public ImageView children;
+        TextView sectionTitle;
+        private LinearLayout container;
+        private View view;
+
+        TOCRowViewHolder(View itemView) {
+            super(itemView);
+            view = itemView;
+            children = (ImageView) itemView.findViewById(R.id.children);
+            container = (LinearLayout) itemView.findViewById(R.id.container);
+            children.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (callback != null) callback.onExpanded(getAdapterPosition());
+                }
+            });
+
+            sectionTitle = (TextView) itemView.findViewById(R.id.section_title);
+            view.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (callback != null) callback.onTocClicked(getAdapterPosition());
+                }
+            });
+        }
+    }
+
+    private static int getPaddingPixels(Context context, int dpValue) {
+        // Get the screen's density scale
+        final float scale = context.getResources().getDisplayMetrics().density;
+        // Convert the dps to pixels, based on density scale
+        return (int) (dpValue * scale + 0.5f);
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/presenter/TOCMvpView.java b/Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/presenter/TOCMvpView.java
new file mode 100755 (executable)
index 0000000..799790d
--- /dev/null
@@ -0,0 +1,15 @@
+package com.folioreader.ui.tableofcontents.presenter;
+
+import com.folioreader.model.TOCLinkWrapper;
+import com.folioreader.ui.base.BaseMvpView;
+
+import java.util.ArrayList;
+
+/**
+ * @author gautam chibde on 8/6/17.
+ */
+
+public interface TOCMvpView extends BaseMvpView {
+
+    void onLoadTOC(ArrayList<TOCLinkWrapper> tocLinkWrapperList);
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/presenter/TableOfContentsPresenter.java b/Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/presenter/TableOfContentsPresenter.java
new file mode 100755 (executable)
index 0000000..c7a2ee1
--- /dev/null
@@ -0,0 +1,85 @@
+package com.folioreader.ui.tableofcontents.presenter;
+
+import com.folioreader.model.TOCLinkWrapper;
+import com.folioreader.ui.base.ManifestCallBack;
+import com.folioreader.ui.base.ManifestTask;
+
+import org.readium.r2_streamer.model.publication.EpubPublication;
+import org.readium.r2_streamer.model.publication.link.Link;
+import org.readium.r2_streamer.model.tableofcontents.TOCLink;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author gautam chibde on 8/6/17.
+ */
+
+public class TableOfContentsPresenter implements ManifestCallBack {
+
+    private TOCMvpView tocMvpView;
+
+    public TableOfContentsPresenter(TOCMvpView tocMvpView) {
+        this.tocMvpView = tocMvpView;
+    }
+
+    public void getTOCContent(String url) {
+        new ManifestTask(this).execute(url);
+    }
+
+    /**
+     * [RECURSIVE]
+     * <p>
+     * function generates list of {@link TOCLinkWrapper} of TOC list from publication manifest
+     *
+     * @param tocLink     table of content elements
+     * @param indentation level of hierarchy of the child elements
+     * @return generated {@link TOCLinkWrapper} list
+     */
+    private static TOCLinkWrapper createTocLinkWrapper(TOCLink tocLink, int indentation) {
+        TOCLinkWrapper tocLinkWrapper = new TOCLinkWrapper(tocLink, indentation);
+        if (tocLink.getTocLinks() != null && !tocLink.getTocLinks().isEmpty()) {
+            for (TOCLink tocLink1 : tocLink.getTocLinks()) {
+                TOCLinkWrapper tocLinkWrapper1 = createTocLinkWrapper(tocLink1, indentation + 1);
+                if (tocLinkWrapper1.getIndentation() != 3) {
+                    tocLinkWrapper.addChild(tocLinkWrapper1);
+                }
+            }
+        }
+        return tocLinkWrapper;
+    }
+
+    private static ArrayList<TOCLinkWrapper> createTOCFromSpine(List<Link> spine) {
+        ArrayList<TOCLinkWrapper> tocLinkWrappers = new ArrayList<>();
+        for (Link link : spine) {
+            TOCLink tocLink = new TOCLink();
+            tocLink.bookTitle = link.bookTitle;
+            tocLink.href = link.href;
+            tocLinkWrappers.add(new TOCLinkWrapper(tocLink, 0));
+        }
+        return tocLinkWrappers;
+    }
+
+    @Override
+    public void onReceivePublication(EpubPublication publication) {
+        if (publication != null) {
+            if (publication.tableOfContents != null) {
+                ArrayList<TOCLinkWrapper> tocLinkWrappers = new ArrayList<>();
+                for (TOCLink tocLink : publication.tableOfContents) {
+                    TOCLinkWrapper tocLinkWrapper = createTocLinkWrapper(tocLink, 0);
+                    tocLinkWrappers.add(tocLinkWrapper);
+                }
+                tocMvpView.onLoadTOC(tocLinkWrappers);
+            } else {
+                tocMvpView.onLoadTOC(createTOCFromSpine(publication.spines));
+            }
+        } else {
+            tocMvpView.onError();
+        }
+    }
+
+    @Override
+    public void onError() {
+        tocMvpView.onError();
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/view/TableOfContentFragment.java b/Android/folioreader/src/main/java/com/folioreader/ui/tableofcontents/view/TableOfContentFragment.java
new file mode 100755 (executable)
index 0000000..6b32fef
--- /dev/null
@@ -0,0 +1,121 @@
+package com.folioreader.ui.tableofcontents.view;\r
+\r
+import android.app.Activity;\r
+import android.content.Intent;\r
+import android.os.Bundle;\r
+import android.support.annotation.Nullable;\r
+import android.support.v4.app.Fragment;\r
+import android.support.v4.content.ContextCompat;\r
+import android.support.v7.widget.DividerItemDecoration;\r
+import android.support.v7.widget.LinearLayoutManager;\r
+import android.support.v7.widget.RecyclerView;\r
+import android.view.LayoutInflater;\r
+import android.view.View;\r
+import android.view.ViewGroup;\r
+import android.widget.TextView;\r
+\r
+import com.folioreader.Config;\r
+import com.folioreader.Constants;\r
+import com.folioreader.R;\r
+import com.folioreader.model.TOCLinkWrapper;\r
+import com.folioreader.ui.tableofcontents.adapter.TOCAdapter;\r
+import com.folioreader.ui.tableofcontents.presenter.TOCMvpView;\r
+import com.folioreader.ui.tableofcontents.presenter.TableOfContentsPresenter;\r
+import com.folioreader.util.AppUtil;\r
+\r
+import java.util.ArrayList;\r
+\r
+import static com.folioreader.Constants.BOOK_TITLE;\r
+import static com.folioreader.Constants.CHAPTER_SELECTED;\r
+import static com.folioreader.Constants.SELECTED_CHAPTER_POSITION;\r
+import static com.folioreader.Constants.TYPE;\r
+\r
+public class TableOfContentFragment extends Fragment implements TOCMvpView, TOCAdapter.TOCCallback {\r
+    private TOCAdapter mTOCAdapter;\r
+    private RecyclerView mTableOfContentsRecyclerView;\r
+    private TableOfContentsPresenter presenter;\r
+    private TextView errorView;\r
+    private Config mConfig;\r
+    private String mBookTitle;\r
+\r
+    public static TableOfContentFragment newInstance(String selectedChapterHref, String bookTitle) {\r
+        TableOfContentFragment tableOfContentFragment = new TableOfContentFragment();\r
+        Bundle args = new Bundle();\r
+        args.putString(SELECTED_CHAPTER_POSITION, selectedChapterHref);\r
+        args.putString(BOOK_TITLE, bookTitle);\r
+        tableOfContentFragment.setArguments(args);\r
+        return tableOfContentFragment;\r
+    }\r
+\r
+    @Override\r
+    public void onCreate(@Nullable Bundle savedInstanceState) {\r
+        super.onCreate(savedInstanceState);\r
+        presenter = new TableOfContentsPresenter(this);\r
+    }\r
+\r
+    @Override\r
+    public View onCreateView(LayoutInflater inflater,\r
+                             ViewGroup container, Bundle savedInstanceState) {\r
+        View mRootView = inflater.inflate(R.layout.fragment_contents, container, false);\r
+        mConfig = AppUtil.getSavedConfig(getActivity());\r
+        mBookTitle = getArguments().getString(BOOK_TITLE);\r
+\r
+        View recyclerView = mRootView.findViewById(R.id.recycler_view_menu);\r
+        if (mConfig.isNightMode()) {\r
+            recyclerView.setBackgroundColor(ContextCompat.getColor(getActivity(), R.color.dark_night));\r
+        }else{\r
+            recyclerView.setBackgroundColor(ContextCompat.getColor(getActivity(), R.color.white));\r
+        }\r
+        return mRootView;\r
+    }\r
+\r
+    @Override\r
+    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {\r
+        super.onViewCreated(view, savedInstanceState);\r
+        mTableOfContentsRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view_menu);\r
+        errorView = (TextView) view.findViewById(R.id.tv_error);\r
+        String urlString = Constants.LOCALHOST + mBookTitle + "/manifest";\r
+\r
+        configRecyclerViews();\r
+        presenter.getTOCContent(urlString);\r
+    }\r
+\r
+    public void configRecyclerViews() {\r
+        mTableOfContentsRecyclerView.setHasFixedSize(true);\r
+        mTableOfContentsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));\r
+        mTableOfContentsRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL));\r
+    }\r
+\r
+    @Override\r
+    public void onLoadTOC(ArrayList<TOCLinkWrapper> tocLinkWrapperList) {\r
+        mTOCAdapter = new TOCAdapter(getActivity(), tocLinkWrapperList, getArguments().getString(SELECTED_CHAPTER_POSITION), mConfig);\r
+        mTOCAdapter.setCallback(this);\r
+        mTableOfContentsRecyclerView.setAdapter(mTOCAdapter);\r
+    }\r
+\r
+    @Override\r
+    public void onError() {\r
+        errorView.setVisibility(View.VISIBLE);\r
+        mTableOfContentsRecyclerView.setVisibility(View.GONE);\r
+        errorView.setText("Table of content \n not found");\r
+    }\r
+\r
+    @Override\r
+    public void onTocClicked(int position) {\r
+        TOCLinkWrapper tocLinkWrapper = (TOCLinkWrapper) mTOCAdapter.getItemAt(position);\r
+        Intent intent = new Intent();\r
+        intent.putExtra(SELECTED_CHAPTER_POSITION, tocLinkWrapper.getTocLink().href);\r
+        intent.putExtra(BOOK_TITLE, tocLinkWrapper.getTocLink().bookTitle);\r
+        intent.putExtra(TYPE, CHAPTER_SELECTED);\r
+        getActivity().setResult(Activity.RESULT_OK, intent);\r
+        getActivity().finish();\r
+    }\r
+\r
+    @Override\r
+    public void onExpanded(int position) {\r
+        TOCLinkWrapper tocLinkWrapper = (TOCLinkWrapper) mTOCAdapter.getItemAt(position);\r
+        if (tocLinkWrapper.getChildren() != null && tocLinkWrapper.getChildren().size() > 0) {\r
+            mTOCAdapter.toggleGroup(position);\r
+        }\r
+    }\r
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/AppUtil.java b/Android/folioreader/src/main/java/com/folioreader/util/AppUtil.java
new file mode 100755 (executable)
index 0000000..01f3aa5
--- /dev/null
@@ -0,0 +1,222 @@
+package com.folioreader.util;
+
+import android.content.Context;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+
+import com.folioreader.Config;
+import com.folioreader.Constants;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.URLConnection;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+import static com.folioreader.Constants.BOOK_STATE;
+import static com.folioreader.Constants.BOOK_TITLE;
+import static com.folioreader.Constants.VIEWPAGER_COUNT;
+import static com.folioreader.Constants.VIEWPAGER_POSITION;
+import static com.folioreader.Constants.WEBVIEW_SCROLL_POSITION;
+import static com.folioreader.util.SharedPreferenceUtil.getSharedPreferencesString;
+
+/**
+ * Created by mahavir on 5/7/16.
+ */
+public class AppUtil {
+
+    private static final String SMIL_ELEMENTS = "smil_elements";
+    private static final String TAG = AppUtil.class.getSimpleName();
+    private static final String FOLIO_READER_ROOT = "folioreader";
+
+    private enum FileType {
+        OPS,
+        OEBPS,
+        NONE
+    }
+
+    public static Map<String, String> toMap(String jsonString) {
+        Map<String, String> map = new HashMap<String, String>();
+        try {
+            JSONArray jsonArray = new JSONArray(jsonString);
+            JSONObject jObject = jsonArray.getJSONObject(0);
+            Iterator<String> keysItr = jObject.keys();
+        while(keysItr.hasNext()) {
+            String key = keysItr.next();
+            Object value = null;
+            value = jObject.get(key);
+
+            if(value instanceof JSONObject) {
+                value = toMap(value.toString());
+            }
+            map.put(key, value.toString());
+        }
+        } catch (JSONException e) {
+            Log.e(TAG, "toMap failed", e);
+        }
+        return map;
+    }
+
+    public static String charsetNameForURLConnection(URLConnection connection) {
+        // see https://stackoverflow.com/a/3934280/1027646
+        String contentType = connection.getContentType();
+        String[] values = contentType.split(";");
+        String charset = null;
+
+        for (String value : values) {
+            value = value.trim();
+
+            if (value.toLowerCase().startsWith("charset=")) {
+                charset = value.substring("charset=".length());
+                break;
+            }
+        }
+
+        if (charset == null || charset.isEmpty()) {
+            charset = "UTF-8"; //Assumption
+        }
+
+        return charset;
+    }
+
+    public static String formatDate(Date hightlightDate) {
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(Constants.DATE_FORMAT, Locale.getDefault());
+        return simpleDateFormat.format(hightlightDate);
+    }
+
+    public static void saveBookState(Context context, String bookTitle, int folioPageViewPagerPosition, int folioPagesViewPagerCount, int
+            webViewScrollPosition) {
+        SharedPreferenceUtil.removeSharedPreferencesKey(context, bookTitle + BOOK_STATE);
+        JSONObject obj = new JSONObject();
+        try {
+            obj.put(BOOK_TITLE, bookTitle);
+            obj.put(WEBVIEW_SCROLL_POSITION, webViewScrollPosition);
+            obj.put(VIEWPAGER_POSITION, folioPageViewPagerPosition);
+            obj.put(VIEWPAGER_COUNT, folioPagesViewPagerCount);
+            SharedPreferenceUtil.
+                    putSharedPreferencesString(
+                            context, bookTitle + BOOK_STATE, obj.toString());
+        } catch (JSONException e) {
+            Log.e(TAG, e.getMessage());
+        }
+    }
+
+    public static void removeBookState(Context context, String bookTitle) {
+        SharedPreferenceUtil.removeSharedPreferencesKey(context, bookTitle + BOOK_STATE);
+    }
+
+    public static boolean checkPreviousBookStateExist(Context context, String bookName) {
+        String json
+                = getSharedPreferencesString(
+                context, bookName + BOOK_STATE,
+                null);
+        if (json != null) {
+            try {
+                JSONObject jsonObject = new JSONObject(json);
+                String bookTitle = jsonObject.getString(BOOK_TITLE);
+                if (bookTitle.equals(bookName))
+                    return true;
+            } catch (JSONException e) {
+                Log.e(TAG, e.getMessage());
+                return false;
+            }
+        }
+        return false;
+    }
+
+    public static int getPreviousBookStatePosition(Context context, String bookName) {
+        String json
+                = getSharedPreferencesString(context,
+                bookName + BOOK_STATE,
+                null);
+        if (json != null) {
+            try {
+                JSONObject jsonObject = new JSONObject(json);
+                return jsonObject.getInt(VIEWPAGER_POSITION);
+            } catch (JSONException e) {
+                Log.e(TAG, e.getMessage());
+                return 0;
+            }
+        }
+        return 0;
+    }
+
+    public static int getPreviousBookStateCount(Context context, String bookName) {
+        String json
+                = getSharedPreferencesString(context,
+                bookName + BOOK_STATE,
+                null);
+        if (json != null) {
+            try {
+                JSONObject jsonObject = new JSONObject(json);
+                return jsonObject.getInt(VIEWPAGER_COUNT);
+            } catch (JSONException e) {
+                Log.e(TAG, e.getMessage());
+                return 0;
+            }
+        }
+        return 0;
+    }
+
+    public static int getPreviousBookStateWebViewPosition(Context context, String bookTitle) {
+        String json = getSharedPreferencesString(context, bookTitle + BOOK_STATE, null);
+        if (json != null) {
+            try {
+                JSONObject jsonObject = new JSONObject(json);
+                return jsonObject.getInt(WEBVIEW_SCROLL_POSITION);
+            } catch (JSONException e) {
+                Log.e(TAG, e.getMessage());
+                return 0;
+            }
+        }
+        return 0;
+    }
+
+
+    public static void saveConfig(Context context, Config config) {
+        JSONObject obj = new JSONObject();
+        try {
+            obj.put(Config.CONFIG_FONT, config.getFont());
+            obj.put(Config.CONFIG_FONT_SIZE, config.getFontSize());
+            obj.put(Config.CONFIG_MARGIN_SIZE, config.getMarginSize());
+            obj.put(Config.CONFIG_INTERLINE_SIZE, config.getInterlineSize());
+            obj.put(Config.CONFIG_IS_NIGHTMODE, config.isNightMode());
+            obj.put(Config.CONFIG_THEMECOLOR, config.getThemeColor());
+            obj.put(Config.CONFIG_IS_TTS,config.isShowTts());
+            obj.put(Config.CONFIG_ICON_COLOR,config.getIconColor());
+            obj.put(Config.CONFIG_TOOLBAR_COLOR,config.getToolbarColor());
+            SharedPreferenceUtil.
+                    putSharedPreferencesString(
+                            context, Config.INTENT_CONFIG, obj.toString());
+        } catch (JSONException e) {
+            Log.e(TAG, e.getMessage(), e);
+        }
+    }
+
+    public static Config getSavedConfig(Context context) {
+        String json = getSharedPreferencesString(context, Config.INTENT_CONFIG, null);
+        if (json != null) {
+            try {
+                JSONObject jsonObject = new JSONObject(json);
+                return new Config(jsonObject);
+            } catch (JSONException e) {
+                Log.e(TAG, e.getMessage());
+                return null;
+            }
+        }
+        return null;
+    }
+}
+
+
+
+
+
+
+
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/FileUtil.java b/Android/folioreader/src/main/java/com/folioreader/util/FileUtil.java
new file mode 100755 (executable)
index 0000000..abd7326
--- /dev/null
@@ -0,0 +1,114 @@
+package com.folioreader.util;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.os.Environment;
+import android.util.Log;
+
+import com.folioreader.Constants;
+import com.folioreader.ui.folio.activity.FolioActivity;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Created by Mahavir on 12/15/16.
+ */
+
+public class FileUtil {
+    private static final String TAG = FileUtil.class.getSimpleName();
+    private static final String FOLIO_READER_ROOT = "/folioreader/";
+
+    public static String saveEpubFileAndLoadLazyBook(final Context context, FolioActivity.EpubSourceType epubSourceType, String epubFilePath, int epubRawId, String epubFileName) {
+        String filePath;
+        InputStream epubInputStream;
+        boolean isFolderAvailable;
+        try {
+            isFolderAvailable = isFolderAvailable(epubFileName);
+            filePath = getFolioEpubFilePath(epubSourceType, epubFilePath, epubFileName);
+
+            if (!isFolderAvailable) {
+                if (epubSourceType.equals(FolioActivity.EpubSourceType.RAW)) {
+                    epubInputStream = context.getResources().openRawResource(epubRawId);
+                    saveTempEpubFile(filePath, epubFileName, epubInputStream);
+                } else if (epubSourceType.equals(FolioActivity.EpubSourceType.ASSETS)) {
+                    AssetManager assetManager = context.getAssets();
+                    epubFilePath = epubFilePath.replaceAll(Constants.ASSET, "");
+                    epubInputStream = assetManager.open(epubFilePath);
+                    saveTempEpubFile(filePath, epubFileName, epubInputStream);
+                } else {
+                    filePath = epubFilePath;
+                }
+            }
+            return filePath;
+        } catch (IOException e) {
+            Log.d(TAG, e.getMessage());
+        }
+
+        return null;
+    }
+
+    public static String getFolioEpubFolderPath(String epubFileName) {
+        return Environment.getExternalStorageDirectory().getAbsolutePath()
+                + "/" + FOLIO_READER_ROOT + "/" + epubFileName;
+    }
+
+    public static String getFolioEpubFilePath(FolioActivity.EpubSourceType sourceType, String epubFilePath, String epubFileName) {
+        if (FolioActivity.EpubSourceType.SD_CARD.equals(sourceType)) {
+            return epubFilePath;
+        } else {
+            return getFolioEpubFolderPath(epubFileName) + "/" + epubFileName + ".epub";
+        }
+    }
+
+    private static boolean isFolderAvailable(String epubFileName) {
+        File file = new File(getFolioEpubFolderPath(epubFileName));
+        return file.isDirectory();
+    }
+
+    public static String getEpubFilename(Context context, FolioActivity.EpubSourceType epubSourceType,
+                                         String epubFilePath, int epubRawId) {
+        String epubFileName;
+        if (epubSourceType.equals(FolioActivity.EpubSourceType.RAW)) {
+            Resources res = context.getResources();
+            epubFileName = res.getResourceEntryName(epubRawId);
+        } else {
+            String[] temp = epubFilePath.split("/");
+            epubFileName = temp[temp.length - 1];
+            int fileMaxIndex = epubFileName.length();
+            epubFileName = epubFileName.substring(0, fileMaxIndex - 5);
+        }
+
+        return epubFileName;
+    }
+
+    public static Boolean saveTempEpubFile(String filePath, String fileName, InputStream inputStream) {
+        OutputStream outputStream = null;
+        File file = new File(filePath);
+        try {
+            if (!file.exists()) {
+                File folder = new File(getFolioEpubFolderPath(fileName));
+                folder.mkdirs();
+
+                outputStream = new FileOutputStream(file);
+                int read = 0;
+                byte[] bytes = new byte[inputStream.available()];
+
+                while ((read = inputStream.read(bytes)) != -1) {
+                    outputStream.write(bytes, 0, read);
+                }
+            } else {
+                return true;
+            }
+            inputStream.close();
+            outputStream.close();
+        } catch (IOException e) {
+            Log.d(TAG, e.getMessage());
+        }
+        return false;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/FolioReader.java b/Android/folioreader/src/main/java/com/folioreader/util/FolioReader.java
new file mode 100755 (executable)
index 0000000..d29dc88
--- /dev/null
@@ -0,0 +1,134 @@
+package com.folioreader.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.support.v4.content.LocalBroadcastManager;
+
+import com.folioreader.Config;
+import com.folioreader.Constants;
+import com.folioreader.model.HighLight;
+import com.folioreader.model.HighlightImpl;
+import com.folioreader.model.sqlite.DbAdapter;
+import com.folioreader.ui.base.OnSaveHighlight;
+import com.folioreader.ui.base.SaveReceivedHighlightTask;
+import com.folioreader.ui.folio.activity.FolioActivity;
+
+import java.util.List;
+
+/**
+ * Created by avez raj on 9/13/2017.
+ */
+
+public class FolioReader {
+    public static final String INTENT_BOOK_ID = "book_id";
+    private Context context;
+
+    private OnHighlightListener onHighlightListener;
+
+    public FolioReader(Context context) {
+        this.context = context;
+        new DbAdapter(context);
+        LocalBroadcastManager.getInstance(context).registerReceiver(highlightReceiver,
+                new IntentFilter(HighlightImpl.BROADCAST_EVENT));
+    }
+
+    private BroadcastReceiver highlightReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            HighlightImpl highlightImpl = intent.getParcelableExtra(HighlightImpl.INTENT);
+            HighLight.HighLightAction action = (HighLight.HighLightAction)
+                    intent.getSerializableExtra(HighLight.HighLightAction.class.getName());
+            if (onHighlightListener != null && highlightImpl != null && action != null) {
+                onHighlightListener.onHighlight(highlightImpl, action);
+            }
+        }
+    };
+
+    public void openBook(String assetOrSdcardPath) {
+        Intent intent = getIntentFromUrl(assetOrSdcardPath, 0);
+        context.startActivity(intent);
+    }
+
+    public void openBook(int rawId) {
+        Intent intent = getIntentFromUrl(null, rawId);
+        context.startActivity(intent);
+    }
+
+    public void openBook(String assetOrSdcardPath, Config config) {
+        Intent intent = getIntentFromUrl(assetOrSdcardPath, 0);
+        intent.putExtra(Config.INTENT_CONFIG, config);
+        context.startActivity(intent);
+    }
+
+    public void openBook(int rawId, Config config) {
+        Intent intent = getIntentFromUrl(null, rawId);
+        intent.putExtra(Config.INTENT_CONFIG, config);
+        context.startActivity(intent);
+    }
+
+    public void openBook(String assetOrSdcardPath, Config config, int port) {
+        Intent intent = getIntentFromUrl(assetOrSdcardPath, 0);
+        intent.putExtra(Config.INTENT_CONFIG, config);
+        intent.putExtra(Config.INTENT_PORT, port);
+        context.startActivity(intent);
+    }
+
+    public void openBook(int rawId, Config config, int port) {
+        Intent intent = getIntentFromUrl(null, rawId);
+        intent.putExtra(Config.INTENT_CONFIG, config);
+        intent.putExtra(Config.INTENT_PORT, port);
+        context.startActivity(intent);
+    }
+
+    public void openBook(String assetOrSdcardPath, Config config, int port, String bookId) {
+        Intent intent = getIntentFromUrl(assetOrSdcardPath, 0);
+        intent.putExtra(Config.INTENT_CONFIG, config);
+        intent.putExtra(Config.INTENT_PORT, port);
+        intent.putExtra(INTENT_BOOK_ID, bookId);
+        context.startActivity(intent);
+    }
+
+    public void openBook(int rawId, Config config, int port, String bookId) {
+        Intent intent = getIntentFromUrl(null, rawId);
+        intent.putExtra(Config.INTENT_CONFIG, config);
+        intent.putExtra(Config.INTENT_PORT, port);
+        intent.putExtra(INTENT_BOOK_ID, bookId);
+        context.startActivity(intent);
+    }
+
+    public Intent createBookIntent(String assetOrSdcardPath, Config config) {
+        Intent intent = getIntentFromUrl(assetOrSdcardPath, 0);
+        intent.putExtra(Config.INTENT_CONFIG, config);
+        return intent;
+    }
+
+    private Intent getIntentFromUrl(String assetOrSdcardPath, int rawId) {
+        Intent intent = new Intent(context, FolioActivity.class);
+        if (rawId != 0) {
+            intent.putExtra(FolioActivity.INTENT_EPUB_SOURCE_PATH, rawId);
+            intent.putExtra(FolioActivity.INTENT_EPUB_SOURCE_TYPE, FolioActivity.EpubSourceType.RAW);
+        } else if (assetOrSdcardPath.contains(Constants.ASSET)) {
+            intent.putExtra(FolioActivity.INTENT_EPUB_SOURCE_PATH, assetOrSdcardPath);
+            intent.putExtra(FolioActivity.INTENT_EPUB_SOURCE_TYPE, FolioActivity.EpubSourceType.ASSETS);
+        } else {
+            intent.putExtra(FolioActivity.INTENT_EPUB_SOURCE_PATH, assetOrSdcardPath);
+            intent.putExtra(FolioActivity.INTENT_EPUB_SOURCE_TYPE, FolioActivity.EpubSourceType.SD_CARD);
+        }
+        return intent;
+    }
+
+    public void registerHighlightListener(OnHighlightListener onHighlightListener) {
+        this.onHighlightListener = onHighlightListener;
+    }
+
+    public void unregisterHighlightListener() {
+        LocalBroadcastManager.getInstance(context).unregisterReceiver(highlightReceiver);
+        this.onHighlightListener = null;
+    }
+
+    public void saveReceivedHighLights(List<HighLight> highlights, OnSaveHighlight onSaveHighlight) {
+        new SaveReceivedHighlightTask(onSaveHighlight, highlights).execute();
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/HighlightUtil.java b/Android/folioreader/src/main/java/com/folioreader/util/HighlightUtil.java
new file mode 100755 (executable)
index 0000000..7d36684
--- /dev/null
@@ -0,0 +1,130 @@
+package com.folioreader.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+import com.folioreader.model.HighLight;
+import com.folioreader.model.HighlightImpl;
+import com.folioreader.model.sqlite.HighLightTable;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.List;
+
+/**
+ * Created by priyank on 5/12/16.
+ */
+public class HighlightUtil {
+
+    private static final String TAG = "HighlightUtil";
+
+    public static String createHighlightRangy(Context context,
+                                              String content,
+                                              String bookTitle,
+                                              String pageId,
+                                              int pageNo,
+                                              String oldRangy) {
+        try {
+            JSONObject jObject = new JSONObject(content);
+
+            String rangy = jObject.getString("rangy");
+            String textContent = jObject.getString("content");
+            String color = jObject.getString("color");
+
+            String rangyHighlightElement = getRangyString(rangy, oldRangy);
+
+            HighlightImpl highlightImpl = new HighlightImpl();
+            highlightImpl.setContent(textContent);
+            highlightImpl.setType(color);
+            highlightImpl.setPageNumber(pageNo);
+            highlightImpl.setBookId(bookTitle);
+            highlightImpl.setPageId(pageId);
+            highlightImpl.setRangy(rangyHighlightElement);
+            highlightImpl.setDate(Calendar.getInstance().getTime());
+            // save highlight to database
+            long id = HighLightTable.insertHighlight(highlightImpl);
+            if (id != -1) {
+                highlightImpl.setId((int) id);
+                sendHighlightBroadcastEvent(context, highlightImpl, HighLight.HighLightAction.NEW);
+            }
+            return rangy;
+        } catch (JSONException e) {
+            Log.e(TAG, "createHighlightRangy failed", e);
+        }
+        return "";
+    }
+
+    /**
+     * function extracts rangy element corresponding to latest highlight.
+     *
+     * @param rangy    new rangy string generated after adding new highlight.
+     * @param oldRangy rangy string before new highlight.
+     * @return rangy element corresponding to latest element.
+     */
+    private static String getRangyString(String rangy, String oldRangy) {
+        List<String> rangyList = getRangyArray(rangy);
+        for (String firs : getRangyArray(oldRangy)) {
+            if (rangyList.contains(firs)) {
+                rangyList.remove(firs);
+            }
+        }
+        if (rangyList.size() >= 1) {
+            return rangyList.get(0);
+        } else {
+            return "";
+        }
+    }
+
+    /**
+     * function converts Rangy text into each individual element
+     * splitting with '|'.
+     *
+     * @param rangy rangy test with format: type:textContent|start$end$id$class$containerId
+     * @return ArrayList of each rangy element corresponding to each highlight
+     */
+    private static List<String> getRangyArray(String rangy) {
+        List<String> rangyElementList = new ArrayList<>();
+        rangyElementList.addAll(Arrays.asList(rangy.split("\\|")));
+        if (rangyElementList.contains("type:textContent")) {
+            rangyElementList.remove("type:textContent");
+        } else if (rangyElementList.contains("")) {
+            return new ArrayList<>();
+        }
+        return rangyElementList;
+    }
+
+    public static String generateRangyString(String pageId) {
+        List<String> rangyList = HighLightTable.getHighlightsForPageId(pageId);
+        StringBuilder builder = new StringBuilder();
+        if (!rangyList.isEmpty()) {
+            builder.append("type:textContent");
+            for (String rangy : rangyList) {
+                builder.append('|');
+                builder.append(rangy);
+            }
+        }
+        return builder.toString();
+    }
+
+    public static void sendHighlightBroadcastEvent(Context context,
+                                                   HighlightImpl highlightImpl,
+                                                   HighLight.HighLightAction action) {
+        LocalBroadcastManager.getInstance(context).sendBroadcast(
+                getHighlightBroadcastIntent(highlightImpl, action));
+    }
+
+    public static Intent getHighlightBroadcastIntent(HighlightImpl highlightImpl,
+                                                     HighLight.HighLightAction modify) {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(HighlightImpl.INTENT, highlightImpl);
+        bundle.putSerializable(HighLight.HighLightAction.class.getName(), modify);
+        return new Intent(HighlightImpl.BROADCAST_EVENT).putExtras(bundle);
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/MultiLevelExpIndListAdapter.java b/Android/folioreader/src/main/java/com/folioreader/util/MultiLevelExpIndListAdapter.java
new file mode 100755 (executable)
index 0000000..882d425
--- /dev/null
@@ -0,0 +1,348 @@
+package com.folioreader.util;
+
+import android.support.v7.widget.RecyclerView;
+
+import com.folioreader.model.TOCLinkWrapper;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Multi-level expandable indentable list adapter.
+ * Initially all elements in the list are single items. When you want to collapse an item and all its
+ * descendants call {@link #collapseGroup(int)}. When you want to exapand a group call {@link #expandGroup(int)}.
+ * Note that groups inside other groups are kept collapsed.
+ *
+ * To collapse an item and all its descendants or expand a group at a certain position
+ * you can call {@link #toggleGroup(int)}.
+ *
+ * To preserve state (i.e. which items are collapsed) when a configuration change happens (e.g. screen rotation)
+ * you should call {@link #saveGroups()} inside onSaveInstanceState and save the returned value into
+ * the Bundle. When the activity/fragment is recreated you can call {@link #restoreGroups(List)}
+ * to restore the previous state. The actual data (e.g. the comments in the sample app) is not preserved,
+ * so you should save it yourself with a static field or implementing Parcelable or using setRetainInstance(true)
+ * or saving data to a file or something like that.
+ *
+ * To see an example of how to extend this abstract class see MyAdapter.java in sampleapp.
+ */
+public abstract class MultiLevelExpIndListAdapter extends RecyclerView.Adapter {
+    /**
+     * Indicates whether or not the observers must be notified whenever
+     * {@link #mData} is modified.
+     */
+    private boolean mNotifyOnChange;
+
+    /**
+     * List of items to display.
+     */
+    private List<ExpIndData> mData;
+
+    /**
+     * Map an item to the relative group.
+     * e.g.: if the user click on item 6 then mGroups(item(6)) = {all items/groups below item 6}
+     */
+    private HashMap<ExpIndData, List<? extends ExpIndData>> mGroups;
+
+    /**
+     * Interface that every item to be displayed has to implement. If an object implements
+     * this interface it means that it can be expanded/collapsed and has a level of indentation.
+     * Note: some methods are commented out because they're not used here, but they should be
+     * implemented if you want your data to be expandable/collapsible and indentable.
+     * See MyComment in the sample app to see an example of how to implement this.
+     */
+    public interface ExpIndData {
+        /**
+         * @return The children of this item.
+         */
+        List<? extends ExpIndData> getChildren();
+
+        /**
+         * @return True if this item is a group.
+         */
+        boolean isGroup();
+
+        /**
+         * @param value True if this item is a group
+         */
+        void setIsGroup(boolean value);
+
+        /**
+         * @param groupSize Set the number of items in the group.
+         *                  Note: groups contained in other groups are counted just as one, not
+         *                        as the number of items that they contain.
+         */
+        void setGroupSize(int groupSize);
+
+        /** Note: actually this method is never called in MultiLevelExpIndListAdapter,
+         * that's why it's not strictly required that you implement this function and so
+         * it's commented out.
+         * @return The number of items in the group.
+         *         Note: groups contained in other groups are counted just as one, not
+         *               as the number of items that they contain.
+         */
+        //int getGroupSize();
+
+        /** Note: actually this method is never called in MultiLevelExpIndListAdapter,
+         * that's why it's not strictly required that you implement this function and so
+         * it's commented out.
+         * @return The level of indentation in the range [0, n-1]
+         */
+        //int getIndentation();
+
+        /** Note: actually this method is never called in MultiLevelExpIndListAdapter,
+         * that's why it's not strictly required that you implement this function and so
+         * it's commented out.
+         * @param indentation The level of indentation in the range [0, n-1]
+         */
+        //int setIndentation(int indentation);
+    }
+
+    public MultiLevelExpIndListAdapter() {
+        mData = new ArrayList<ExpIndData>();
+        mGroups = new HashMap<ExpIndData, List<? extends ExpIndData>>();
+        mNotifyOnChange = true;
+    }
+
+    public MultiLevelExpIndListAdapter(ArrayList<TOCLinkWrapper> tocLinkWrappers) {
+        mData = new ArrayList<ExpIndData>();
+        mGroups = new HashMap<ExpIndData, List<? extends ExpIndData>>();
+        mNotifyOnChange = true;
+        mData.addAll(tocLinkWrappers);
+        collapseAllTOCLinks(tocLinkWrappers);
+    }
+
+    public void add(ExpIndData item) {
+        if (item != null) {
+            mData.add(item);
+            if (mNotifyOnChange)
+                notifyItemChanged(mData.size() - 1);
+        }
+    }
+
+    public void addAll(int position, Collection<? extends ExpIndData> data) {
+        if (data != null && data.size() > 0) {
+            mData.addAll(position, data);
+            if (mNotifyOnChange)
+                notifyItemRangeInserted(position, data.size());
+        }
+    }
+
+    public void addAll(Collection<? extends ExpIndData> data) {
+        addAll(mData.size(), data);
+    }
+
+    public void insert(int position, ExpIndData item) {
+        mData.add(position, item);
+        if (mNotifyOnChange)
+            notifyItemInserted(position);
+    }
+
+    /**
+     * Clear all items and groups.
+     */
+    public void clear() {
+        if (mData.size() > 0) {
+            int size = mData.size();
+            mData.clear();
+            mGroups.clear();
+            if (mNotifyOnChange)
+                notifyItemRangeRemoved(0, size);
+        }
+    }
+
+    /**
+     * Remove an item or group.If it's a group it removes also all the
+     * items and groups that it contains.
+     * @param item The item or group to be removed.
+     * @return true if this adapter was modified by this operation, false otherwise.
+     */
+    public boolean remove(ExpIndData item) {
+        return remove(item, false);
+    }
+
+    /**
+     * Remove an item or group. If it's a group it removes also all the
+     * items and groups that it contains if expandGroupBeforeRemoval is false.
+     * If it's true the group is expanded and then only the item is removed.
+     * @param item The item or group to be removed.
+     * @param expandGroupBeforeRemoval True to expand the group before removing the item.
+     *                                 False to remove also all the items and groups contained if
+     *                                 the item to be removed is a group.
+     * @return true if this adapter was modified by this operation, false otherwise.
+     */
+    public boolean remove(ExpIndData item, boolean expandGroupBeforeRemoval) {
+        int index;
+        boolean removed = false;
+        if (item != null && (index = mData.indexOf(item)) != -1 && (removed = mData.remove(item))) {
+            if (mGroups.containsKey(item)) {
+                if (expandGroupBeforeRemoval)
+                    expandGroup(index);
+                mGroups.remove(item);
+            }
+            if (mNotifyOnChange)
+                notifyItemRemoved(index);
+        }
+        return removed;
+    }
+
+    public ExpIndData getItemAt(int position) {
+        return mData.get(position);
+    }
+
+    @Override
+    public int getItemCount() {
+        return mData.size();
+    }
+
+    /**
+     * Expand the group at position "posititon".
+     * @param position The position (range [0,n-1]) of the group that has to be expanded
+     */
+    public void expandGroup(int position) {
+        ExpIndData firstItem = getItemAt(position);
+
+        if (!firstItem.isGroup()) {
+            return;
+        }
+
+        // get the group of the descendants of firstItem
+        List<? extends ExpIndData> group = mGroups.remove(firstItem);
+
+        firstItem.setIsGroup(false);
+        firstItem.setGroupSize(0);
+
+        notifyItemChanged(position);
+        addAll(position + 1, group);
+    }
+
+    /**
+     * Collapse the descendants of the item at position "position".
+     * @param position The position (range [0,n-1]) of the element that has to be collapsed
+     */
+    public void collapseGroup(int position) {
+        ExpIndData firstItem = getItemAt(position);
+
+        if (firstItem.getChildren() == null || firstItem.getChildren().isEmpty())
+            return;
+
+        // group containing all the descendants of firstItem
+        List<ExpIndData> group = new ArrayList<ExpIndData>();
+        // stack for depth first search
+        List<ExpIndData> stack = new ArrayList<ExpIndData>();
+        int groupSize = 0;
+
+        for (int i = firstItem.getChildren().size() - 1; i >= 0; i--) {
+            stack.add(firstItem.getChildren().get(i));
+        }
+
+        while (!stack.isEmpty()) {
+            ExpIndData item = stack.remove(stack.size() - 1);
+            group.add(item);
+            groupSize++;
+            // stop when the item is a leaf or a group
+            if (item.getChildren() != null && !item.getChildren().isEmpty() && !item.isGroup()) {
+                for (int i = item.getChildren().size() - 1; i >= 0; i--) {
+                    stack.add(item.getChildren().get(i));
+                }
+            }
+
+            if (mData.contains(item)) mData.remove(item);
+        }
+
+        mGroups.put(firstItem, group);
+        firstItem.setIsGroup(true);
+        firstItem.setGroupSize(groupSize);
+
+        notifyItemChanged(position);
+        notifyItemRangeRemoved(position + 1, groupSize);
+    }
+
+    private void collapseAllTOCLinks(ArrayList<TOCLinkWrapper> tocLinkWrappers){
+        if (tocLinkWrappers == null || tocLinkWrappers.isEmpty()) return;
+
+        for (TOCLinkWrapper tocLinkWrapper:tocLinkWrappers) {
+            groupTOCLink(tocLinkWrapper);
+            collapseAllTOCLinks(tocLinkWrapper.getTocLinkWrappers());
+        }
+    }
+
+    private void groupTOCLink(TOCLinkWrapper tocLinkWrapper){
+        // group containing all the descendants of firstItem
+        List<ExpIndData> group = new ArrayList<ExpIndData>();
+        int groupSize = 0;
+        if (tocLinkWrapper.getChildren()!=null && !tocLinkWrapper.getChildren().isEmpty()) {
+            group.addAll(tocLinkWrapper.getChildren());
+            groupSize = tocLinkWrapper.getChildren().size();
+        }
+        // stack for depth first search
+        //List<ExpIndData> stack = new ArrayList<ExpIndData>();
+        //int groupSize = 0;
+
+        /*for (int i = tocLinkWrapper.getChildren().size() - 1; i >= 0; i--)
+            stack.add(tocLinkWrapper.getChildren().get(i));
+
+        while (!stack.isEmpty()) {
+            ExpIndData item = stack.remove(stack.size() - 1);
+            group.add(item);
+            groupSize++;
+            // stop when the item is a leaf or a group
+            if (item.getChildren() != null && !item.getChildren().isEmpty() && !item.isGroup()) {
+                for (int i = item.getChildren().size() - 1; i >= 0; i--)
+                    stack.add(item.getChildren().get(i));
+            }
+        }*/
+
+        mGroups.put(tocLinkWrapper, group);
+        tocLinkWrapper.setIsGroup(true);
+        tocLinkWrapper.setGroupSize(groupSize);
+    }
+    /**
+     * Collpase/expand the item at position "position"
+     * @param position The position (range [0,n-1]) of the element that has to be collapsed/expanded
+     */
+    public void toggleGroup(int position) {
+        if (getItemAt(position).isGroup()){
+            expandGroup(position);
+        } else {
+            collapseGroup(position);
+        }
+    }
+
+    /**
+     * In onSaveInstanceState, you should save the groups' indices returned by this function
+     * in the Bundle so that later they can be restored using {@link #restoreGroups(List)}.
+     * saveGroups() expand all the groups so you should call this function only inside onSaveInstanceState.
+     * @return A list of indices of items that are groups.
+     */
+    public ArrayList<Integer> saveGroups() {
+        boolean notify = mNotifyOnChange;
+        mNotifyOnChange = false;
+        ArrayList<Integer> groupsIndices = new ArrayList<Integer>();
+        for (int i = 0; i < mData.size(); i++) {
+            if (mData.get(i).isGroup()) {
+                expandGroup(i);
+                groupsIndices.add(i);
+            }
+        }
+        mNotifyOnChange = notify;
+        return groupsIndices;
+    }
+
+    /**
+     * Call this function to restore the groups that were collapsed before the configuration change
+     * happened (e.g. screen rotation). See {@link #saveGroups()}.
+     * @param groupsNum The list of indices of items that are groups and should be collapsed.
+     */
+    public void restoreGroups(List<Integer> groupsNum) {
+        if (groupsNum == null)
+            return;
+        boolean notify = mNotifyOnChange;
+        mNotifyOnChange = false;
+        for (int i = groupsNum.size() - 1; i >= 0; i--) {
+            collapseGroup(groupsNum.get(i));
+        }
+        mNotifyOnChange = notify;
+    }
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/OnHighlightListener.java b/Android/folioreader/src/main/java/com/folioreader/util/OnHighlightListener.java
new file mode 100755 (executable)
index 0000000..0d4970f
--- /dev/null
@@ -0,0 +1,21 @@
+package com.folioreader.util;
+
+import com.folioreader.model.HighLight;
+import com.folioreader.model.HighlightImpl;
+
+/**
+ * Interface to convey highlight events.
+ *
+ * @author gautam chibde on 26/9/17.
+ */
+
+public interface OnHighlightListener {
+
+    /**
+     * This method will be invoked when a highlight is created, deleted or modified.
+     *
+     * @param highlight meta-data for created highlight {@link HighlightImpl}.
+     * @param type      type of event e.g new,edit or delete {@link com.folioreader.model.HighlightImpl.HighLightAction}.
+     */
+    void onHighlight(HighLight highlight, HighLight.HighLightAction type);
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/ProgressDialog.java b/Android/folioreader/src/main/java/com/folioreader/util/ProgressDialog.java
new file mode 100755 (executable)
index 0000000..344584a
--- /dev/null
@@ -0,0 +1,19 @@
+package com.folioreader.util;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.widget.TextView;
+
+import com.folioreader.R;
+
+public class ProgressDialog {
+
+    public static Dialog show(Context ctx, String text) {
+        final Dialog dialog = new Dialog(ctx, R.style.full_screen_dialog);
+        dialog.setContentView(R.layout.progress_dialog);
+        ((TextView) dialog.findViewById(R.id.label_loading)).setText(text);
+        dialog.setCancelable(false);
+        dialog.show();
+        return dialog;
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/SMILParser.java b/Android/folioreader/src/main/java/com/folioreader/util/SMILParser.java
new file mode 100755 (executable)
index 0000000..09ced63
--- /dev/null
@@ -0,0 +1,111 @@
+package com.folioreader.util;
+
+import com.folioreader.model.media_overlay.OverlayItems;
+
+import org.readium.r2_streamer.parser.EpubParser;
+import org.readium.r2_streamer.parser.EpubParserException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author gautam chibde on 20/6/17.
+ */
+
+public final class SMILParser {
+
+    /**
+     * Function creates list {@link OverlayItems} of all tag elements from the
+     * input html raw string.
+     *
+     * @param html raw html string
+     * @return list of {@link OverlayItems}
+     */
+    public static List<OverlayItems> parseSMIL(String html) {
+        List<OverlayItems> mediaItems = new ArrayList<>();
+        try {
+            Document document = EpubParser.xmlParser(html);
+            NodeList sections = document.getDocumentElement().getElementsByTagName("section");
+            for (int i = 0; i < sections.getLength(); i++) {
+                parseNodes(mediaItems, (Element) sections.item(i));
+            }
+        } catch (EpubParserException e) {
+            return new ArrayList<>();
+        }
+        return mediaItems;
+    }
+
+    /**
+     * [RECURSIVE]
+     * Function recursively finds and parses the child elements of the input
+     * DOM element.
+     *
+     * @param names   input {@link OverlayItems} where data is to be stored
+     * @param section input DOM element
+     */
+    private static void parseNodes(List<OverlayItems> names, Element section) {
+        for (Node n = section.getFirstChild(); n != null; n = n.getNextSibling()) {
+            if (n.getNodeType() == Node.ELEMENT_NODE) {
+                Element e = (Element) n;
+                if (e.hasAttribute("id")) {
+                    names.add(new OverlayItems(e.getAttribute("id"), e.getTagName()));
+                } else {
+                    parseNodes(names, e);
+                }
+            }
+        }
+    }
+
+    /**
+     * function finds all the text content inside input html page and splits each sentence
+     * with separator '.' and returns them as a list of {@link OverlayItems}
+     *
+     * @param html input raw html
+     * @return generated {@link OverlayItems}
+     */
+    public static List<OverlayItems> parseSMILForTTS(String html) {
+        List<OverlayItems> mediaItems = new ArrayList<>();
+        try {
+            Document document = EpubParser.xmlParser(html);
+            NodeList sections = document.getDocumentElement().getElementsByTagName("body");
+            for (int i = 0; i < sections.getLength(); i++) {
+                parseNodesTTS(mediaItems, (Element) sections.item(i));
+            }
+        } catch (EpubParserException e) {
+            return new ArrayList<>();
+        }
+        return mediaItems;
+    }
+
+    /**
+     * [RECURSIVE]
+     * Function recursively looks for the child element with the text content and
+     * adds them to the input {@link OverlayItems} list
+     *
+     * @param names   input {@link OverlayItems} where data is to be stored
+     * @param section input DOM element
+     */
+    private static void parseNodesTTS(List<OverlayItems> names, Element section) {
+        for (Node n = section.getFirstChild(); n != null; n = n.getNextSibling()) {
+            if (n.getNodeType() == Node.ELEMENT_NODE) {
+                Element e = (Element) n;
+                for (Node n1 = e.getFirstChild(); n1 != null; n1 = n1.getNextSibling()) {
+                    if (n1.getTextContent() != null) {
+                        for (String s : n1.getTextContent().split("\\.")) {
+                            if (!s.isEmpty()) {
+                                OverlayItems i = new OverlayItems();
+                                i.setText(s);
+                                names.add(i);
+                            }
+                        }
+                    }
+                }
+                parseNodesTTS(names, e);
+            }
+        }
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/ScreenUtils.java b/Android/folioreader/src/main/java/com/folioreader/util/ScreenUtils.java
new file mode 100755 (executable)
index 0000000..fd398c6
--- /dev/null
@@ -0,0 +1,56 @@
+package com.folioreader.util;\r
+\r
+import android.content.Context;\r
+import android.util.DisplayMetrics;\r
+import android.view.Display;\r
+import android.view.WindowManager;\r
+\r
+/**\r
+ * Created by arthur on 06/10/16.\r
+ */\r
+public class ScreenUtils {\r
+\r
+    private Context ctx;\r
+    private DisplayMetrics metrics;\r
+\r
+    public ScreenUtils(Context ctx) {\r
+        this.ctx = ctx;\r
+        WindowManager wm = (WindowManager) ctx\r
+                .getSystemService(Context.WINDOW_SERVICE);\r
+\r
+        Display display = wm.getDefaultDisplay();\r
+        metrics = new DisplayMetrics();\r
+        display.getMetrics(metrics);\r
+\r
+    }\r
+\r
+    public int getHeight() {\r
+        return metrics.heightPixels;\r
+    }\r
+\r
+    public int getWidth() {\r
+        return metrics.widthPixels;\r
+    }\r
+\r
+    public int getRealHeight() {\r
+        return metrics.heightPixels / metrics.densityDpi;\r
+    }\r
+\r
+    public int getRealWidth() {\r
+        return metrics.widthPixels / metrics.densityDpi;\r
+    }\r
+\r
+    public int getDensity() {\r
+        return metrics.densityDpi;\r
+    }\r
+\r
+    public int getScale(int picWidth) {\r
+        Display display\r
+                = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE))\r
+                .getDefaultDisplay();\r
+        int width = display.getWidth();\r
+        Double val = new Double(width) / new Double(picWidth);\r
+        val = val * 100d;\r
+        return val.intValue();\r
+    }\r
+}\r
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/SharedPreferenceUtil.java b/Android/folioreader/src/main/java/com/folioreader/util/SharedPreferenceUtil.java
new file mode 100755 (executable)
index 0000000..94ed08b
--- /dev/null
@@ -0,0 +1,94 @@
+package com.folioreader.util;\r
+\r
+import android.content.Context;\r
+import android.content.SharedPreferences;\r
+import android.preference.PreferenceManager;\r
+\r
+import java.util.Set;\r
+\r
+/**\r
+ * Created by PC on 6/9/2016.\r
+ */\r
+public class SharedPreferenceUtil {\r
+    public static final String SENT_TOKEN_TO_SERVER = "sentTokenToServer";\r
+    public static final String REGISTRATION_COMPLETE = "registrationComplete";\r
+\r
+    public static void putSharedPreferencesInt(Context context, String key, int value) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        SharedPreferences.Editor edit = preferences.edit();\r
+        edit.putInt(key, value);\r
+        edit.commit();\r
+    }\r
+\r
+    public static void putSharedPreferencesBoolean(Context context, String key, boolean val) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        SharedPreferences.Editor edit = preferences.edit();\r
+        edit.putBoolean(key, val);\r
+        edit.commit();\r
+    }\r
+\r
+    public static void putSharedPreferencesString(Context context, String key, String val) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        SharedPreferences.Editor edit = preferences.edit();\r
+        edit.putString(key, val);\r
+        edit.commit();\r
+    }\r
+\r
+    public static void putSharedPreferencesStringSet(Context context, String key, Set<String> val) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        SharedPreferences.Editor editor = preferences.edit();\r
+        editor.putStringSet(key, val);\r
+        editor.commit();\r
+    }\r
+\r
+    public static void putSharedPreferencesFloat(Context context, String key, float val) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        SharedPreferences.Editor edit = preferences.edit();\r
+        edit.putFloat(key, val);\r
+        edit.commit();\r
+    }\r
+\r
+    public static void putSharedPreferencesLong(Context context, String key, long val) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        SharedPreferences.Editor edit = preferences.edit();\r
+        edit.putLong(key, val);\r
+        edit.commit();\r
+    }\r
+\r
+    public static long getSharedPreferencesLong(Context context, String key, long defaultValue) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        return preferences.getLong(key, defaultValue);\r
+    }\r
+\r
+    public static float getSharedPreferencesFloat(Context context, String key, float defaultValue) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        return preferences.getFloat(key, defaultValue);\r
+    }\r
+\r
+    public static String getSharedPreferencesString(Context context, String key, String defaultValue) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        return preferences.getString(key, defaultValue);\r
+    }\r
+\r
+    public static Set<String> getSharedPreferencesStringSet(Context context, String key, Set<String> defaultValue) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        return preferences.getStringSet(key, defaultValue);\r
+    }\r
+\r
+    public static int getSharedPreferencesInt(Context context, String key, int defaultValue) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        return preferences.getInt(key, defaultValue);\r
+    }\r
+\r
+    public static boolean getSharedPreferencesBoolean(Context context, String key, boolean defaultValue) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        return preferences.getBoolean(key, defaultValue);\r
+    }\r
+\r
+    public static boolean removeSharedPreferencesKey(Context context, String key) {\r
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);\r
+        SharedPreferences.Editor editor = preferences.edit();\r
+        editor.remove(key);\r
+        return editor.commit();\r
+    }\r
+}\r
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/StyleableTextView.java b/Android/folioreader/src/main/java/com/folioreader/util/StyleableTextView.java
new file mode 100755 (executable)
index 0000000..3cb7483
--- /dev/null
@@ -0,0 +1,33 @@
+package com.folioreader.util;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatTextView;
+import android.util.AttributeSet;
+
+import com.folioreader.R;
+
+public class StyleableTextView extends AppCompatTextView {
+
+       public StyleableTextView(Context context, String font) {
+               super(context);
+               setCustomFont(context, font);
+       }
+
+       public StyleableTextView(Context context, AttributeSet attrs) {
+               super(context, attrs);
+               UiUtil.setCustomFont(this, context, attrs,
+                               R.styleable.StyleableTextView,
+                               R.styleable.StyleableTextView_folio_font);
+       }
+
+       public StyleableTextView(Context context, AttributeSet attrs, int defStyle) {
+               super(context, attrs, defStyle);
+               UiUtil.setCustomFont(this, context, attrs,
+                R.styleable.StyleableTextView,
+                R.styleable.StyleableTextView_folio_font);
+       }
+
+       private void setCustomFont(Context context, String font){
+               UiUtil.setCustomFont(this, context, font);
+       }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/util/UiUtil.java b/Android/folioreader/src/main/java/com/folioreader/util/UiUtil.java
new file mode 100755 (executable)
index 0000000..7676ed6
--- /dev/null
@@ -0,0 +1,178 @@
+package com.folioreader.util;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.PorterDuff;
+import android.graphics.Typeface;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.StateSet;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.folioreader.R;
+import com.folioreader.view.UnderlinedTextView;
+
+import java.lang.ref.SoftReference;
+import java.util.Hashtable;
+
+/**
+ * Created by mahavir on 3/30/16.
+ */
+public class UiUtil {
+    public static void setCustomFont(View view, Context ctx, AttributeSet attrs,
+                                     int[] attributeSet, int fontId) {
+        TypedArray a = ctx.obtainStyledAttributes(attrs, attributeSet);
+        String customFont = a.getString(fontId);
+        setCustomFont(view, ctx, customFont);
+        a.recycle();
+    }
+
+    public static boolean setCustomFont(View view, Context ctx, String asset) {
+        if (TextUtils.isEmpty(asset))
+            return false;
+        Typeface tf = null;
+        try {
+            tf = getFont(ctx, asset);
+            if (view instanceof TextView) {
+                ((TextView) view).setTypeface(tf);
+            } else {
+                ((Button) view).setTypeface(tf);
+            }
+        } catch (Exception e) {
+            Log.e("AppUtil", "Could not get typface  " + asset);
+            return false;
+        }
+
+        return true;
+    }
+
+    private static final Hashtable<String, SoftReference<Typeface>> fontCache = new Hashtable<String, SoftReference<Typeface>>();
+
+    public static Typeface getFont(Context c, String name) {
+        synchronized (fontCache) {
+            if (fontCache.get(name) != null) {
+                SoftReference<Typeface> ref = fontCache.get(name);
+                if (ref.get() != null) {
+                    return ref.get();
+                }
+            }
+
+            Typeface typeface = Typeface.createFromAsset(c.getAssets(), name);
+            fontCache.put(name, new SoftReference<Typeface>(typeface));
+
+            return typeface;
+        }
+    }
+
+    public static ColorStateList getColorList(Context context, int selectedColor, int unselectedColor) {
+        int[][] states = new int[][]{
+                new int[]{android.R.attr.state_selected},
+                new int[]{}
+        };
+        int[] colors = new int[]{
+                ContextCompat.getColor(context, selectedColor),
+                ContextCompat.getColor(context, unselectedColor)
+        };
+        ColorStateList list = new ColorStateList(states, colors);
+        return list;
+    }
+
+    public static void keepScreenAwake(boolean enable, Context context) {
+        if (enable) {
+            ((Activity) context)
+                    .getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        } else {
+            ((Activity) context)
+                    .getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        }
+    }
+
+    public static void setBackColorToTextView(UnderlinedTextView textView, String type) {
+        Context context = textView.getContext();
+        if (type.equals("yellow")) {
+            setUnderLineColor(textView, context, R.color.yellow, R.color.yellow);
+        } else if (type.equals("green")) {
+            setUnderLineColor(textView, context, R.color.green, R.color.green);
+        } else if (type.equals("blue")) {
+            setUnderLineColor(textView, context, R.color.blue, R.color.blue);
+        } else if (type.equals("pink")) {
+            setUnderLineColor(textView, context, R.color.pink, R.color.pink);
+        } else if (type.equals("underline")) {
+            setUnderLineColor(textView, context, android.R.color.transparent, android.R.color.holo_red_dark);
+            textView.setUnderlineWidth(2.0f);
+        }
+    }
+
+
+    private static void setUnderLineColor(UnderlinedTextView underlinedTextView, Context context, int background,int underlinecolor) {
+        underlinedTextView.setBackgroundColor(ContextCompat.getColor(context,
+                background));
+        underlinedTextView.setUnderLineColor(ContextCompat.getColor(context,
+                underlinecolor));
+    }
+
+    public static float convertDpToPixel(float dp, Context context) {
+        Resources resources = context.getResources();
+        DisplayMetrics metrics = resources.getDisplayMetrics();
+        float px = dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
+        return px;
+    }
+
+    public static void copyToClipboard(Context context, String text) {
+        ClipboardManager clipboard =
+                (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+        ClipData clip = ClipData.newPlainText("copy", text);
+        clipboard.setPrimaryClip(clip);
+    }
+
+    public static void share(Context context, String text) {
+        Intent sendIntent = new Intent();
+        sendIntent.setAction(Intent.ACTION_SEND);
+        sendIntent.putExtra(Intent.EXTRA_TEXT, text);
+        sendIntent.setType("text/plain");
+        context.startActivity(Intent.createChooser(sendIntent,
+                context.getResources().getText(R.string.send_to)));
+    }
+
+
+    public static void setColorToImage(Context context, int color, Drawable drawable) {
+        drawable.setColorFilter(ContextCompat.getColor(context, color), PorterDuff.Mode.SRC_ATOP);
+    }
+
+
+    public static StateListDrawable convertColorIntoStateDrawable(Context context, int colorSelected, int colorNormal) {
+        StateListDrawable stateListDrawable = new StateListDrawable();
+        stateListDrawable.addState(new int[]{android.R.attr.state_selected}, new ColorDrawable(ContextCompat.getColor(context, colorSelected)));
+        stateListDrawable.addState(StateSet.WILD_CARD, new ColorDrawable(ContextCompat.getColor(context, colorNormal)));
+        return stateListDrawable;
+    }
+
+    public static GradientDrawable getShapeDrawable(Context context, int color) {
+        GradientDrawable gradientDrawable = new GradientDrawable();
+        gradientDrawable.setShape(GradientDrawable.RECTANGLE);
+        gradientDrawable.setStroke(pxToDp(2), ContextCompat.getColor(context, color));
+        gradientDrawable.setColor(ContextCompat.getColor(context, color));
+        gradientDrawable.setCornerRadius(pxToDp(3));
+        return gradientDrawable;
+    }
+
+    public static int pxToDp(int px) {
+        return (int) (px / Resources.getSystem().getDisplayMetrics().density);
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/view/ConfigBottomSheetDialogFragment.java b/Android/folioreader/src/main/java/com/folioreader/view/ConfigBottomSheetDialogFragment.java
new file mode 100755 (executable)
index 0000000..adaecd4
--- /dev/null
@@ -0,0 +1,366 @@
+package com.folioreader.view;
+
+import android.animation.Animator;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.design.widget.BottomSheetBehavior;
+import android.support.design.widget.BottomSheetDialog;
+import android.support.design.widget.BottomSheetDialogFragment;
+import android.support.design.widget.CoordinatorLayout;
+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.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+
+import com.folioreader.Config;
+import com.folioreader.Constants;
+import com.folioreader.R;
+import com.folioreader.model.event.BusOwner;
+import com.folioreader.model.event.ReloadDataEvent;
+import com.folioreader.ui.folio.activity.ToolbarUtils;
+import com.folioreader.util.AppUtil;
+import com.folioreader.util.UiUtil;
+
+
+/**
+ * Created by mobisys2 on 11/16/2016.
+ */
+
+public class ConfigBottomSheetDialogFragment extends BottomSheetDialogFragment implements View.OnClickListener {
+
+    public static final int DAY_BUTTON = 30;
+    public static final int NIGHT_BUTTON = 31;
+    private static final int FADE_DAY_NIGHT_MODE = 500;
+
+    private CoordinatorLayout.Behavior mBehavior;
+    private boolean mIsNightMode = false;
+
+
+    private RelativeLayout mContainer;
+    private ImageView mDayButton;
+    private ImageView mNightButton;
+    private SeekBar mFontSizeSeekBar;
+    private SeekBar mMarginSizeSeekBar;
+    private SeekBar mInterlineSizeSeekBar;
+    private View mDialogView;
+    private ConfigDialogCallback mConfigDialogCallback;
+    private Config mConfig;
+
+    public interface ConfigDialogCallback {
+        void onOrientationChange(int orentation);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.view_config, container);
+    }
+
+    @Override
+    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                BottomSheetDialog dialog = (BottomSheetDialog) getDialog();
+                FrameLayout bottomSheet = (FrameLayout)
+                        dialog.findViewById(android.support.design.R.id.design_bottom_sheet);
+                BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
+                behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+                behavior.setPeekHeight(0);
+            }
+        });
+
+        mDialogView = view;
+        mConfig = AppUtil.getSavedConfig(getActivity());
+        initViews();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mDialogView.getViewTreeObserver().addOnGlobalLayoutListener(null);
+    }
+
+    private void initViews() {
+        inflateView();
+        configFonts();
+        mFontSizeSeekBar.setProgress(mConfig.getFontSize());
+        mMarginSizeSeekBar.setProgress(mConfig.getMarginSize());
+        mInterlineSizeSeekBar.setProgress(mConfig.getInterlineSize());
+        configSeekBars();
+        selectFont(mConfig.getFont(), false);
+        mIsNightMode = mConfig.isNightMode();
+
+        if (mIsNightMode) {
+            mContainer.setBackgroundColor(ContextCompat.getColor(getActivity(), R.color.night));
+            mDayButton.setSelected(false);
+            mNightButton.setSelected(true);
+            UiUtil.setColorToImage(getActivity(), mConfig.getThemeColor(), mNightButton.getDrawable());
+            UiUtil.setColorToImage(getActivity(), R.color.app_gray, mDayButton.getDrawable());
+        } else {
+            mContainer.setBackgroundColor(ContextCompat.getColor(getActivity(), R.color.white));
+            mDayButton.setSelected(true);
+            mNightButton.setSelected(false);
+            UiUtil.setColorToImage(getActivity(), mConfig.getThemeColor(), mDayButton.getDrawable());
+            UiUtil.setColorToImage(getActivity(), R.color.app_gray, mNightButton.getDrawable());
+        }
+
+        mConfigDialogCallback = (ConfigDialogCallback) getActivity();
+    }
+
+    private void inflateView() {
+        mContainer = (RelativeLayout) mDialogView.findViewById(R.id.container);
+        mFontSizeSeekBar = (SeekBar) mDialogView.findViewById(R.id.seekbar_font_size);
+           mMarginSizeSeekBar = mDialogView.findViewById(R.id.seekbar_margin_size);
+           mInterlineSizeSeekBar = mDialogView.findViewById(R.id.seekbar_interline_size);
+           mDayButton = (ImageView) mDialogView.findViewById(R.id.day_button);
+        mNightButton = (ImageView) mDialogView.findViewById(R.id.night_button);
+        mDayButton.setTag(DAY_BUTTON);
+        mNightButton.setTag(NIGHT_BUTTON);
+        mDayButton.setOnClickListener(this);
+        mNightButton.setOnClickListener(this);
+    }
+
+
+    private void configFonts() {
+        ((StyleableTextView) mDialogView.findViewById(R.id.btn_font_ebgaramond)).setTextColor(UiUtil.getColorList(getActivity(), mConfig.getThemeColor(), R.color.grey_color));
+        ((StyleableTextView) mDialogView.findViewById(R.id.btn_font_lato)).setTextColor(UiUtil.getColorList(getActivity(), mConfig.getThemeColor(), R.color.grey_color));
+        ((StyleableTextView) mDialogView.findViewById(R.id.btn_font_lora)).setTextColor(UiUtil.getColorList(getActivity(), mConfig.getThemeColor(), R.color.grey_color));
+        ((StyleableTextView) mDialogView.findViewById(R.id.btn_font_raleway)).setTextColor(UiUtil.getColorList(getActivity(), mConfig.getThemeColor(), R.color.grey_color));
+        mDialogView.findViewById(R.id.btn_font_ebgaramond).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                selectFont(Constants.FONT_EBGARAMOND, true);
+            }
+        });
+
+        mDialogView.findViewById(R.id.btn_font_lato).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                selectFont(Constants.FONT_LATO, true);
+            }
+        });
+
+        mDialogView.findViewById(R.id.btn_font_lora).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                selectFont(Constants.FONT_LORA, true);
+            }
+        });
+
+        mDialogView.findViewById(R.id.btn_font_raleway).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                selectFont(Constants.FONT_RALEWAY, true);
+            }
+        });
+    }
+
+    private void selectFont(int selectedFont, boolean isReloadNeeded) {
+        if (selectedFont == Constants.FONT_EBGARAMOND) {
+            mDialogView.findViewById(R.id.btn_font_ebgaramond).setSelected(true);
+            mDialogView.findViewById(R.id.btn_font_lato).setSelected(false);
+            mDialogView.findViewById(R.id.btn_font_lora).setSelected(false);
+            mDialogView.findViewById(R.id.btn_font_raleway).setSelected(false);
+        } else if (selectedFont == Constants.FONT_LATO) {
+            mDialogView.findViewById(R.id.btn_font_ebgaramond).setSelected(false);
+            mDialogView.findViewById(R.id.btn_font_lato).setSelected(true);
+            mDialogView.findViewById(R.id.btn_font_lora).setSelected(false);
+            mDialogView.findViewById(R.id.btn_font_raleway).setSelected(false);
+        } else if (selectedFont == Constants.FONT_LORA) {
+            mDialogView.findViewById(R.id.btn_font_ebgaramond).setSelected(false);
+            mDialogView.findViewById(R.id.btn_font_lato).setSelected(false);
+            mDialogView.findViewById(R.id.btn_font_lora).setSelected(true);
+            mDialogView.findViewById(R.id.btn_font_raleway).setSelected(false);
+        } else if (selectedFont == Constants.FONT_RALEWAY) {
+            mDialogView.findViewById(R.id.btn_font_ebgaramond).setSelected(false);
+            mDialogView.findViewById(R.id.btn_font_lato).setSelected(false);
+            mDialogView.findViewById(R.id.btn_font_lora).setSelected(false);
+            mDialogView.findViewById(R.id.btn_font_raleway).setSelected(true);
+        }
+
+        mConfig.setFont(selectedFont);
+        //if (mConfigDialogCallback != null) mConfigDialogCallback.onConfigChange();
+        if (isAdded() && isReloadNeeded) {
+            AppUtil.saveConfig(getActivity(),mConfig);
+
+            Activity activity = getActivity();
+            if (activity instanceof BusOwner)
+                ((BusOwner) activity).getBus().post(new ReloadDataEvent());
+        }
+    }
+
+    private void toggleBlackTheme() {
+
+        int day = getResources().getColor(R.color.white);
+        int night = getResources().getColor(R.color.night);
+        int darkNight = getResources().getColor(R.color.dark_night);
+        final int diffNightDark = night - darkNight;
+
+        ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
+                mIsNightMode ? day : night, mIsNightMode ? night : day);
+        colorAnimation.setDuration(FADE_DAY_NIGHT_MODE);
+        colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+
+            @Override
+            public void onAnimationUpdate(ValueAnimator animator) {
+                int value = (int) animator.getAnimatedValue();
+                mContainer.setBackgroundColor(value);
+            }
+        });
+
+        colorAnimation.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animator) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+               // mIsNightMode = !mIsNightMode;
+                mConfig.setNightMode(mIsNightMode);
+                AppUtil.saveConfig(getActivity(),mConfig);
+
+                Activity activity = getActivity();
+                if (activity instanceof BusOwner)
+                    ((BusOwner) activity).getBus().post(new ReloadDataEvent());
+
+                ///mConfigDialogCallback.onConfigChange();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animator) {
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animator) {
+            }
+        });
+
+        colorAnimation.setDuration(FADE_DAY_NIGHT_MODE);
+        colorAnimation.start();
+    }
+
+    private void configSeekBars() {
+           setupSeekBarThumb(mFontSizeSeekBar);
+           setupSeekBarThumb(mMarginSizeSeekBar);
+           setupSeekBarThumb(mInterlineSizeSeekBar);
+
+        mFontSizeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                mConfig.setFontSize(progress);
+                saveConfigAndNotifyBusOwner();
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+            }
+        });
+        mMarginSizeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+               @Override
+               public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                       mConfig.setMarginSize(progress);
+                       saveConfigAndNotifyBusOwner();
+               }
+
+               @Override
+               public void onStartTrackingTouch(SeekBar seekBar) {
+               }
+
+               @Override
+               public void onStopTrackingTouch(SeekBar seekBar) {
+               }
+        });
+        mInterlineSizeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+               @Override
+               public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                       mConfig.setInterlineSize(progress);
+                       saveConfigAndNotifyBusOwner();
+               }
+
+               @Override
+               public void onStartTrackingTouch(SeekBar seekBar) {
+               }
+
+               @Override
+               public void onStopTrackingTouch(SeekBar seekBar) {
+               }
+        });
+    }
+
+       private void setupSeekBarThumb(SeekBar seekBar) {
+               Drawable thumbDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.seekbar_thumb);
+               UiUtil.setColorToImage(getActivity(), mConfig.getThemeColor(), (thumbDrawable));
+               UiUtil.setColorToImage(getActivity(), mConfig.getThemeColor(), seekBar.getProgressDrawable());
+               seekBar.setThumb(thumbDrawable);
+       }
+
+       private void saveConfigAndNotifyBusOwner() {
+           AppUtil.saveConfig(getActivity(),mConfig);
+           Activity activity = getActivity();
+           if (activity instanceof BusOwner) {
+                   ((BusOwner) activity).getBus().post(new ReloadDataEvent());
+           }
+    }
+
+
+    @Override
+    public void onClick(View v) {
+        switch (((Integer) v.getTag())) {
+            case DAY_BUTTON:
+                mIsNightMode = false;
+                toggleBlackTheme();
+                mDayButton.setSelected(true);
+                mNightButton.setSelected(false);
+                setToolBarColor();
+                setAudioPlayerBackground();
+                UiUtil.setColorToImage(getActivity(), R.color.app_gray, mNightButton.getDrawable());
+                UiUtil.setColorToImage(getActivity(), mConfig.getThemeColor(), mDayButton.getDrawable());
+                break;
+            case NIGHT_BUTTON:
+                mIsNightMode = true;
+                toggleBlackTheme();
+                mDayButton.setSelected(false);
+                mNightButton.setSelected(true);
+                UiUtil.setColorToImage(getActivity(), mConfig.getThemeColor(), mNightButton.getDrawable());
+                UiUtil.setColorToImage(getActivity(), R.color.app_gray, mDayButton.getDrawable());
+                setToolBarColor();
+                setAudioPlayerBackground();
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void setToolBarColor() {
+        Toolbar toolbar = ((Activity) getContext()).findViewById(R.id.toolbar);
+        ToolbarUtils.updateToolbarColors(getContext(), toolbar, mConfig, mIsNightMode);
+    }
+
+    private void setAudioPlayerBackground() {
+        if (mIsNightMode) {
+            ((Activity) getContext()).
+                    findViewById(R.id.container).
+                    setBackgroundColor(ContextCompat.getColor(getContext(), R.color.night));
+        } else {
+            ((Activity) getContext()).
+                    findViewById(R.id.container).
+                    setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white));
+        }
+    }
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/view/DirectionalViewpager.java b/Android/folioreader/src/main/java/com/folioreader/view/DirectionalViewpager.java
new file mode 100755 (executable)
index 0000000..574e453
--- /dev/null
@@ -0,0 +1,4119 @@
+package com.folioreader.view;\r
+\r
+/**\r
+ * Created by mobisys on 10/10/2016.\r
+ */\r
+\r
+\r
+import android.content.Context;\r
+import android.content.res.Resources;\r
+import android.content.res.TypedArray;\r
+import android.database.DataSetObserver;\r
+import android.graphics.Canvas;\r
+import android.graphics.Rect;\r
+import android.graphics.drawable.Drawable;\r
+import android.os.Build;\r
+import android.os.Bundle;\r
+import android.os.Parcel;\r
+import android.os.Parcelable;\r
+import android.os.SystemClock;\r
+import android.support.annotation.CallSuper;\r
+import android.support.annotation.DrawableRes;\r
+import android.support.v4.os.ParcelableCompat;\r
+import android.support.v4.os.ParcelableCompatCreatorCallbacks;\r
+import android.support.v4.view.AccessibilityDelegateCompat;\r
+import android.support.v4.view.MotionEventCompat;\r
+import android.support.v4.view.PagerAdapter;\r
+import android.support.v4.view.VelocityTrackerCompat;\r
+import android.support.v4.view.ViewCompat;\r
+import android.support.v4.view.ViewConfigurationCompat;\r
+import android.support.v4.view.WindowInsetsCompat;\r
+import android.support.v4.view.accessibility.AccessibilityEventCompat;\r
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;\r
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;\r
+import android.support.v4.widget.EdgeEffectCompat;\r
+import android.util.AttributeSet;\r
+import android.util.Log;\r
+import android.view.FocusFinder;\r
+import android.view.Gravity;\r
+import android.view.KeyEvent;\r
+import android.view.MotionEvent;\r
+import android.view.SoundEffectConstants;\r
+import android.view.VelocityTracker;\r
+import android.view.View;\r
+import android.view.ViewConfiguration;\r
+import android.view.ViewGroup;\r
+import android.view.ViewParent;\r
+import android.view.accessibility.AccessibilityEvent;\r
+import android.view.animation.Interpolator;\r
+import android.widget.Scroller;\r
+\r
+import com.folioreader.R;\r
+\r
+import java.lang.reflect.Method;\r
+import java.util.ArrayList;\r
+import java.util.Collections;\r
+import java.util.Comparator;\r
+import java.util.List;\r
+\r
+public class DirectionalViewpager extends ViewGroup {\r
+    private static final String TAG = "ViewPager";\r
+    private static final boolean DEBUG = false;\r
+\r
+    private static final boolean USE_CACHE = false;\r
+\r
+    private static final int DEFAULT_OFFSCREEN_PAGES = 1;\r
+    private static final int MAX_SETTLE_DURATION = 600; // ms\r
+    private static final int MIN_DISTANCE_FOR_FLING = 25; // dips\r
+\r
+    private static final int DEFAULT_GUTTER_SIZE = 16; // dips\r
+\r
+    private static final int MIN_FLING_VELOCITY = 400; // dips\r
+\r
+    private static final int[] LAYOUT_ATTRS = new int[]{\r
+            android.R.attr.layout_gravity\r
+    };\r
+\r
+    public static enum Direction {\r
+        HORIZONTAL,\r
+        VERTICAL,\r
+    }\r
+\r
+    /**\r
+     * Used to track what the expected number of items in the adapter should be.\r
+     * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.\r
+     */\r
+    private int mExpectedAdapterCount;\r
+    public String mDirection = Direction.VERTICAL.name();\r
+\r
+    static class ItemInfo {\r
+        Object object;\r
+        int position;\r
+        boolean scrolling;\r
+        float widthFactor;\r
+        float heightFactor;\r
+        float offset;\r
+    }\r
+\r
+    private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>() {\r
+        @Override\r
+        public int compare(ItemInfo lhs, ItemInfo rhs) {\r
+            return lhs.position - rhs.position;\r
+        }\r
+    };\r
+\r
+    private static final Interpolator sInterpolator = new Interpolator() {\r
+        public float getInterpolation(float t) {\r
+            t -= 1.0f;\r
+            return t * t * t * t * t + 1.0f;\r
+        }\r
+    };\r
+\r
+    private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();\r
+    private final ItemInfo mTempItem = new ItemInfo();\r
+\r
+    private final Rect mTempRect = new Rect();\r
+\r
+    private PagerAdapter mAdapter;\r
+    private int mCurItem;   // Index of currently displayed page.\r
+    private int mRestoredCurItem = -1;\r
+    private Parcelable mRestoredAdapterState = null;\r
+    private ClassLoader mRestoredClassLoader = null;\r
+\r
+    private Scroller mScroller;\r
+    private boolean mIsScrollStarted;\r
+\r
+    private PagerObserver mObserver;\r
+\r
+    private int mPageMargin;\r
+    private Drawable mMarginDrawable;\r
+    private int mTopPageBounds;\r
+    private int mBottomPageBounds;\r
+    private int mLeftPageBounds;\r
+    private int mRightPageBounds;\r
+\r
+    // Offsets of the first and last items, if known.\r
+    // Set during population, used to determine if we are at the beginning\r
+    // or end of the pager data set during touch scrolling.\r
+    private float mFirstOffset = -Float.MAX_VALUE;\r
+    private float mLastOffset = Float.MAX_VALUE;\r
+\r
+    private int mChildWidthMeasureSpec;\r
+    private int mChildHeightMeasureSpec;\r
+    private boolean mInLayout;\r
+\r
+    private boolean mScrollingCacheEnabled;\r
+\r
+    private boolean mPopulatePending;\r
+    private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;\r
+\r
+    private boolean mIsBeingDragged;\r
+    private boolean mIsUnableToDrag;\r
+    private boolean mIgnoreGutter;\r
+    private int mDefaultGutterSize;\r
+    private int mGutterSize;\r
+    private int mTouchSlop;\r
+    /**\r
+     * Position of the last motion event.\r
+     */\r
+    private float mLastMotionX;\r
+    private float mLastMotionY;\r
+    private float mInitialMotionX;\r
+    private float mInitialMotionY;\r
+    /**\r
+     * ID of the active pointer. This is used to retain consistency during\r
+     * drags/flings if multiple pointers are used.\r
+     */\r
+    private int mActivePointerId = INVALID_POINTER;\r
+    /**\r
+     * Sentinel value for no current active pointer.\r
+     * Used by {@link #mActivePointerId}.\r
+     */\r
+    private static final int INVALID_POINTER = -1;\r
+\r
+    /**\r
+     * Determines speed during touch scrolling\r
+     */\r
+    private VelocityTracker mVelocityTracker;\r
+    private int mMinimumVelocity;\r
+    private int mMaximumVelocity;\r
+    private int mFlingDistance;\r
+    private int mCloseEnough;\r
+\r
+    // If the pager is at least this close to its final position, complete the scroll\r
+    // on touch down and let the user interact with the content inside instead of\r
+    // "catching" the flinging pager.\r
+    private static final int CLOSE_ENOUGH = 2; // dp\r
+\r
+    private boolean mFakeDragging;\r
+    private long mFakeDragBeginTime;\r
+\r
+    private EdgeEffectCompat mLeftEdge;\r
+    private EdgeEffectCompat mRightEdge;\r
+    private EdgeEffectCompat mTopEdge;\r
+    private EdgeEffectCompat mBottomEdge;\r
+\r
+    private boolean mFirstLayout = true;\r
+    private boolean mNeedCalculatePageOffsets = false;\r
+    private boolean mCalledSuper;\r
+    private int mDecorChildCount;\r
+\r
+    private List<OnPageChangeListener> mOnPageChangeListeners;\r
+    private OnPageChangeListener mOnPageChangeListener;\r
+    private OnPageChangeListener mInternalPageChangeListener;\r
+    private OnAdapterChangeListener mAdapterChangeListener;\r
+    private PageTransformer mPageTransformer;\r
+    private Method mSetChildrenDrawingOrderEnabled;\r
+\r
+    private static final int DRAW_ORDER_DEFAULT = 0;\r
+    private static final int DRAW_ORDER_FORWARD = 1;\r
+    private static final int DRAW_ORDER_REVERSE = 2;\r
+    private int mDrawingOrder;\r
+    private ArrayList<View> mDrawingOrderedChildren;\r
+    private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();\r
+\r
+    /**\r
+     * Indicates that the pager is in an idle, settled state. The current page\r
+     * is fully in view and no animation is in progress.\r
+     */\r
+    public static final int SCROLL_STATE_IDLE = 0;\r
+\r
+    /**\r
+     * Indicates that the pager is currently being dragged by the user.\r
+     */\r
+    public static final int SCROLL_STATE_DRAGGING = 1;\r
+\r
+    /**\r
+     * Indicates that the pager is in the process of settling to a final position.\r
+     */\r
+    public static final int SCROLL_STATE_SETTLING = 2;\r
+\r
+    private final Runnable mEndScrollRunnable = new Runnable() {\r
+        public void run() {\r
+            setScrollState(SCROLL_STATE_IDLE);\r
+            populate();\r
+        }\r
+    };\r
+\r
+    private int mScrollState = SCROLL_STATE_IDLE;\r
+\r
+    /**\r
+     * Callback interface for responding to changing state of the selected page.\r
+     */\r
+    public interface OnPageChangeListener {\r
+\r
+        /**\r
+         * This method will be invoked when the current\r
+         * page is scrolled, either as part\r
+         * of a programmatically initiated\r
+         * smooth scroll or a user initiated touch scroll.\r
+         *\r
+         * @param position             Position index of the first page currently being displayed.\r
+         *                             <p>\r
+         *                             Page position+1 will be visible if positionOffset is nonzero.\r
+         * @param positionOffset\r
+         * Value from [0, 1) indicating the offset from the page at position.\r
+         * @param positionOffsetPixels\r
+         * Value in pixels indicating the offset from position.\r
+         */\r
+        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);\r
+\r
+        /**\r
+         * This method will be invoked when a new page becomes selected. Animation is not\r
+         * necessarily complete.\r
+         *\r
+         * @param position Position index of the new selected page.\r
+         */\r
+        public void onPageSelected(int position);\r
+\r
+        /**\r
+         * Called when the scroll state changes. Useful for discovering when the user\r
+         * begins dragging, when the pager is automatically settling to the current page,\r
+         * or when it is fully stopped/idle.\r
+         *\r
+         * @param state The new scroll state.\r
+         * @see ViewPager#SCROLL_STATE_IDLE\r
+         * @see ViewPager#SCROLL_STATE_DRAGGING\r
+         * @see ViewPager#SCROLL_STATE_SETTLING\r
+         */\r
+        public void onPageScrollStateChanged(int state);\r
+    }\r
+\r
+    /**\r
+     * Simple implementation of the {@link OnPageChangeListener}\r
+     * interface with stub\r
+     * implementations of each method.\r
+     * Extend this if you do not intend to override\r
+     * every method of {@link OnPageChangeListener}.\r
+     */\r
+    public static class SimpleOnPageChangeListener implements OnPageChangeListener {\r
+        @Override\r
+        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {\r
+            // This space for rent\r
+        }\r
+\r
+        @Override\r
+        public void onPageSelected(int position) {\r
+            // This space for rent\r
+        }\r
+\r
+        @Override\r
+        public void onPageScrollStateChanged(int state) {\r
+            // This space for rent\r
+        }\r
+    }\r
+\r
+    /**\r
+     * A PageTransformer is invoked whenever a visible/attached page is scrolled.\r
+     * This offers an opportunity for the application to apply a custom transformation\r
+     * to the page views using animation properties.\r
+     * <p>\r
+     * <p>As property animation is only supported as of Android 3.0 and forward,\r
+     * setting a PageTransformer on a ViewPager on earlier platform versions will\r
+     * be ignored.</p>\r
+     */\r
+    public interface PageTransformer {\r
+        /**\r
+         * Apply a property transformation to the given page.\r
+         *\r
+         * @param page     Apply the transformation to this page\r
+         * @param position Position of page relative to the current front-and-center\r
+         *                 position of the pager. 0 is front and center. 1 is one full\r
+         *                 page position to the right, and -1 is one page position to the left.\r
+         */\r
+        public void transformPage(View page, float position);\r
+    }\r
+\r
+    /**\r
+     * Used internally to monitor when adapters are switched.\r
+     */\r
+    interface OnAdapterChangeListener {\r
+        public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter);\r
+    }\r
+\r
+    /**\r
+     * Used internally to tag special types of child views that should be added as\r
+     * pager decorations by default.\r
+     */\r
+    interface Decor {\r
+    }\r
+\r
+    public DirectionalViewpager(Context context) {\r
+        super(context);\r
+        initViewPager();\r
+    }\r
+\r
+    public DirectionalViewpager(Context context, AttributeSet attrs) {\r
+        super(context, attrs);\r
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DirectionalViewpager);\r
+        if (a.getString(R.styleable.DirectionalViewpager_direction) != null) {\r
+            mDirection = a.getString(R.styleable.DirectionalViewpager_direction);\r
+        }\r
+        initViewPager();\r
+    }\r
+\r
+    void initViewPager() {\r
+        setWillNotDraw(false);\r
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);\r
+        setFocusable(true);\r
+        final Context context = getContext();\r
+        mScroller = new Scroller(context, sInterpolator);\r
+        final ViewConfiguration configuration = ViewConfiguration.get(context);\r
+        final float density = context.getResources().getDisplayMetrics().density;\r
+\r
+        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);\r
+        mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);\r
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();\r
+        mLeftEdge = new EdgeEffectCompat(context);\r
+        mRightEdge = new EdgeEffectCompat(context);\r
+        mTopEdge = new EdgeEffectCompat(context);\r
+        mBottomEdge = new EdgeEffectCompat(context);\r
+\r
+        mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);\r
+        mCloseEnough = (int) (CLOSE_ENOUGH * density);\r
+        mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);\r
+\r
+        ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());\r
+\r
+        if (ViewCompat.getImportantForAccessibility(this)\r
+                == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {\r
+            ViewCompat.setImportantForAccessibility(this,\r
+                    ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);\r
+        }\r
+\r
+        ViewCompat.setOnApplyWindowInsetsListener(this,\r
+                new android.support\r
+                        .v4.view.OnApplyWindowInsetsListener() {\r
+                    private final Rect mTempRect = new Rect();\r
+\r
+                    @Override\r
+                    public WindowInsetsCompat\r
+                            onApplyWindowInsets(final View v,\r
+                                        final WindowInsetsCompat originalInsets) {\r
+                        // First let the ViewPager itself try and consume them...\r
+                        final WindowInsetsCompat applied =\r
+                                    ViewCompat.onApplyWindowInsets(v, originalInsets);\r
+                        if (applied.isConsumed()) {\r
+                            // If the ViewPager consumed all insets, return now\r
+                            return applied;\r
+                        }\r
+\r
+                        // Now we'll manually dispatch the insets to our children. Since ViewPager\r
+                        // children are always full-height, we do not want to use the standard\r
+                        // ViewGroup dispatchApplyWindowInsets since if child 0 consumes them,\r
+                        // the rest of the children will not receive any insets. To workaround this\r
+                        // we manually dispatch the applied insets, not allowing children to\r
+                        // consume them from each other. We do however keep track of any insets\r
+                        // which are consumed, returning the union of our children's consumption\r
+                        final Rect res = mTempRect;\r
+                        res.left = applied.getSystemWindowInsetLeft();\r
+                        res.top = applied.getSystemWindowInsetTop();\r
+                        res.right = applied.getSystemWindowInsetRight();\r
+                        res.bottom = applied.getSystemWindowInsetBottom();\r
+\r
+                        for (int i = 0, count = getChildCount(); i < count; i++) {\r
+                            final WindowInsetsCompat childInsets = ViewCompat\r
+                                    .dispatchApplyWindowInsets(getChildAt(i), applied);\r
+                            // Now keep track of any consumed by tracking each dimension's min\r
+                            // value\r
+                            res.left\r
+                                    = Math.min(childInsets.getSystemWindowInsetLeft(),\r
+                                    res.left);\r
+                            res.top = Math.min(childInsets.getSystemWindowInsetTop(),\r
+                                    res.top);\r
+                            res.right = Math.min(childInsets.getSystemWindowInsetRight(),\r
+                                    res.right);\r
+                            res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(),\r
+                                    res.bottom);\r
+                        }\r
+\r
+                        // Now return a new WindowInsets, using the consumed window insets\r
+                        return applied.replaceSystemWindowInsets(\r
+                                res.left, res.top, res.right, res.bottom);\r
+                    }\r
+                });\r
+    }\r
+\r
+    @Override\r
+    protected void onDetachedFromWindow() {\r
+        removeCallbacks(mEndScrollRunnable);\r
+        // To be on the safe side, abort the scroller\r
+        if ((mScroller != null) && !mScroller.isFinished()) {\r
+            mScroller.abortAnimation();\r
+        }\r
+        super.onDetachedFromWindow();\r
+    }\r
+\r
+    private void setScrollState(int newState) {\r
+        if (mScrollState == newState) {\r
+            return;\r
+        }\r
+\r
+        mScrollState = newState;\r
+        if (mPageTransformer != null) {\r
+            // PageTransformers can do complex things that benefit from hardware layers.\r
+            enableLayers(newState != SCROLL_STATE_IDLE);\r
+        }\r
+        dispatchOnScrollStateChanged(newState);\r
+    }\r
+\r
+    /**\r
+     * Set a PagerAdapter that will supply views for this pager as needed.\r
+     *\r
+     * @param adapter Adapter to use\r
+     */\r
+    public void setAdapter(PagerAdapter adapter) {\r
+        if (mAdapter != null) {\r
+            mAdapter.unregisterDataSetObserver(mObserver);\r
+            mAdapter.startUpdate(this);\r
+            for (int i = 0; i < mItems.size(); i++) {\r
+                final ItemInfo ii = mItems.get(i);\r
+                mAdapter.destroyItem(this, ii.position, ii.object);\r
+            }\r
+            mAdapter.finishUpdate(this);\r
+            mItems.clear();\r
+            removeNonDecorViews();\r
+            mCurItem = 0;\r
+            scrollTo(0, 0);\r
+        }\r
+\r
+        final PagerAdapter oldAdapter = mAdapter;\r
+        mAdapter = adapter;\r
+        mExpectedAdapterCount = 0;\r
+\r
+        if (mAdapter != null) {\r
+            if (mObserver == null) {\r
+                mObserver = new PagerObserver();\r
+            }\r
+            mAdapter.registerDataSetObserver(mObserver);\r
+            mPopulatePending = false;\r
+            final boolean wasFirstLayout = mFirstLayout;\r
+            mFirstLayout = true;\r
+            mExpectedAdapterCount = mAdapter.getCount();\r
+            if (mRestoredCurItem >= 0) {\r
+                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);\r
+                setCurrentItemInternal(mRestoredCurItem, false, true);\r
+                mRestoredCurItem = -1;\r
+                mRestoredAdapterState = null;\r
+                mRestoredClassLoader = null;\r
+            } else if (!wasFirstLayout) {\r
+                populate();\r
+            } else {\r
+                requestLayout();\r
+            }\r
+        }\r
+\r
+        if (mAdapterChangeListener != null && oldAdapter != adapter) {\r
+            mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);\r
+        }\r
+    }\r
+\r
+    private void removeNonDecorViews() {\r
+        for (int i = 0; i < getChildCount(); i++) {\r
+            final View child = getChildAt(i);\r
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+            if (!lp.isDecor) {\r
+                removeViewAt(i);\r
+                i--;\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Retrieve the current adapter supplying pages.\r
+     *\r
+     * @return The currently registered PagerAdapter\r
+     */\r
+    public PagerAdapter getAdapter() {\r
+        return mAdapter;\r
+    }\r
+\r
+    void setOnAdapterChangeListener(OnAdapterChangeListener listener) {\r
+        mAdapterChangeListener = listener;\r
+    }\r
+\r
+    private int getClientWidth() {\r
+        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();\r
+    }\r
+\r
+    private int getClientHeight() {\r
+        return getMeasuredHeight() - getPaddingTop() - getPaddingBottom();\r
+    }\r
+\r
+    /**\r
+     * Set the currently selected page. If the ViewPager has already been through its first\r
+     * layout with its current adapter there will be a smooth animated transition between\r
+     * the current item and the specified item.\r
+     *\r
+     * @param item Item index to select\r
+     */\r
+    public void setCurrentItem(int item) {\r
+        mPopulatePending = false;\r
+        setCurrentItemInternal(item, !mFirstLayout, false);\r
+    }\r
+\r
+    /**\r
+     * Set the currently selected page.\r
+     *\r
+     * @param item         Item index to select\r
+     * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately\r
+     */\r
+    public void setCurrentItem(int item, boolean smoothScroll) {\r
+        mPopulatePending = false;\r
+        setCurrentItemInternal(item, smoothScroll, false);\r
+    }\r
+\r
+    public int getCurrentItem() {\r
+        return mCurItem;\r
+    }\r
+\r
+    public int getExpectedAdapterCount() {\r
+        return mExpectedAdapterCount;\r
+    }\r
+\r
+    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {\r
+        setCurrentItemInternal(item, smoothScroll, always, 0);\r
+    }\r
+\r
+    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {\r
+        if (mAdapter == null || mAdapter.getCount() <= 0) {\r
+            setScrollingCacheEnabled(false);\r
+            return;\r
+        }\r
+        if (!always && mCurItem == item && mItems.size() != 0) {\r
+            setScrollingCacheEnabled(false);\r
+            return;\r
+        }\r
+\r
+        if (item < 0) {\r
+            item = 0;\r
+        } else if (item >= mAdapter.getCount()) {\r
+            item = mAdapter.getCount() - 1;\r
+        }\r
+        final int pageLimit = mOffscreenPageLimit;\r
+        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {\r
+            // We are doing a jump by more than one page.  To avoid\r
+            // glitches, we want to keep all current pages in the view\r
+            // until the scroll ends.\r
+            for (int i = 0; i < mItems.size(); i++) {\r
+                mItems.get(i).scrolling = true;\r
+            }\r
+        }\r
+        final boolean dispatchSelected = mCurItem != item;\r
+\r
+        if (mFirstLayout) {\r
+            // We don't have any idea how big we are yet and shouldn't have any pages either.\r
+            // Just set things up and let the pending layout handle things.\r
+            mCurItem = item;\r
+            if (dispatchSelected) {\r
+                dispatchOnPageSelected(item);\r
+            }\r
+            requestLayout();\r
+        } else {\r
+            populate(item);\r
+            scrollToItem(item, smoothScroll, velocity, dispatchSelected);\r
+        }\r
+    }\r
+\r
+    private void scrollToItem(int item, boolean smoothScroll, int velocity,\r
+                              boolean dispatchSelected) {\r
+        final ItemInfo curInfo = infoForPosition(item);\r
+        int destX = 0;\r
+        int destY = 0;\r
+        if (isHorizontal()) {\r
+            if (curInfo != null) {\r
+                final int width = getClientWidth();\r
+                destX = (int) (width * Math.max(mFirstOffset,\r
+                        Math.min(curInfo.offset, mLastOffset)));\r
+            }\r
+            if (smoothScroll) {\r
+                smoothScrollTo(destX, 0, velocity);\r
+                if (dispatchSelected) {\r
+                    dispatchOnPageSelected(item);\r
+                }\r
+            } else {\r
+                if (dispatchSelected) {\r
+                    dispatchOnPageSelected(item);\r
+                }\r
+                completeScroll(false);\r
+                scrollTo(destX, 0);\r
+                pageScrolled(destX, 0);\r
+            }\r
+        } else {\r
+            if (curInfo != null) {\r
+                final int height = getClientHeight();\r
+                destY = (int) (height * Math.max(mFirstOffset,\r
+                        Math.min(curInfo.offset, mLastOffset)));\r
+            }\r
+            if (smoothScroll) {\r
+                smoothScrollTo(0, destY, velocity);\r
+                if (dispatchSelected && mOnPageChangeListener != null) {\r
+                    mOnPageChangeListener.onPageSelected(item);\r
+                }\r
+                if (dispatchSelected && mInternalPageChangeListener != null) {\r
+                    mInternalPageChangeListener.onPageSelected(item);\r
+                }\r
+            } else {\r
+                if (dispatchSelected && mOnPageChangeListener != null) {\r
+                    mOnPageChangeListener.onPageSelected(item);\r
+                }\r
+                if (dispatchSelected && mInternalPageChangeListener != null) {\r
+                    mInternalPageChangeListener.onPageSelected(item);\r
+                }\r
+                completeScroll(false);\r
+                scrollTo(0, destY);\r
+                pageScrolled(0, destY);\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Set a listener that will be invoked whenever the page changes or is incrementally\r
+     * scrolled. See {@link OnPageChangeListener}.\r
+     *\r
+     * @param listener Listener to set\r
+     * @deprecated Use {@link #addOnPageChangeListener(OnPageChangeListener)}\r
+     * and {@link #removeOnPageChangeListener(OnPageChangeListener)} instead.\r
+     */\r
+    @Deprecated\r
+    public void setOnPageChangeListener(OnPageChangeListener listener) {\r
+        mOnPageChangeListener = listener;\r
+    }\r
+\r
+    /**\r
+     * Add a listener that will be invoked whenever the page changes or is incrementally\r
+     * scrolled. See {@link OnPageChangeListener}.\r
+     * <p>\r
+     * <p>Components that add a listener should take care to remove it when finished.\r
+     * Other components that take ownership of a view may call {@link #clearOnPageChangeListeners()}\r
+     * to remove all attached listeners.</p>\r
+     *\r
+     * @param listener listener to add\r
+     */\r
+    public void addOnPageChangeListener(OnPageChangeListener listener) {\r
+        if (mOnPageChangeListeners == null) {\r
+            mOnPageChangeListeners = new ArrayList<>();\r
+        }\r
+        mOnPageChangeListeners.add(listener);\r
+    }\r
+\r
+    /**\r
+     * Remove a listener that was previously added via\r
+     * {@link #addOnPageChangeListener(OnPageChangeListener)}.\r
+     *\r
+     * @param listener listener to remove\r
+     */\r
+    public void removeOnPageChangeListener(OnPageChangeListener listener) {\r
+        if (mOnPageChangeListeners != null) {\r
+            mOnPageChangeListeners.remove(listener);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Remove all listeners that are notified of any changes in scroll state or position.\r
+     */\r
+    public void clearOnPageChangeListeners() {\r
+        if (mOnPageChangeListeners != null) {\r
+            mOnPageChangeListeners.clear();\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Set a {@link PageTransformer} that will be called for each attached page whenever\r
+     * the scroll position is changed. This allows the application to apply custom property\r
+     * transformations to each page, overriding the default sliding look and feel.\r
+     * <p>\r
+     * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.\r
+     * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p>\r
+     *\r
+     * @param reverseDrawingOrder true if the supplied PageTransformer requires page views\r
+     *                            to be drawn from last to first instead of first to last.\r
+     * @param transformer         PageTransformer that will modify each page's animation properties\r
+     */\r
+    public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) {\r
+        if (Build.VERSION.SDK_INT >= 11) {\r
+            final boolean hasTransformer = transformer != null;\r
+            final boolean needsPopulate = hasTransformer != (mPageTransformer != null);\r
+            mPageTransformer = transformer;\r
+            setChildrenDrawingOrderEnabledCompat(hasTransformer);\r
+            if (hasTransformer) {\r
+                mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;\r
+            } else {\r
+                mDrawingOrder = DRAW_ORDER_DEFAULT;\r
+            }\r
+            if (needsPopulate) populate();\r
+        }\r
+    }\r
+\r
+    void setChildrenDrawingOrderEnabledCompat(boolean enable) {\r
+        if (Build.VERSION.SDK_INT >= 7) {\r
+            if (mSetChildrenDrawingOrderEnabled == null) {\r
+                try {\r
+                    mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod(\r
+                            "setChildrenDrawingOrderEnabled", new Class[]{Boolean.TYPE});\r
+                } catch (NoSuchMethodException e) {\r
+                    Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e);\r
+                }\r
+            }\r
+            try {\r
+                mSetChildrenDrawingOrderEnabled\r
+                        .invoke(this, enable);\r
+            } catch (Exception e) {\r
+                Log.e(TAG, "Error changing children drawing order", e);\r
+            }\r
+        }\r
+    }\r
+\r
+    @Override\r
+    protected int getChildDrawingOrder(int childCount, int i) {\r
+        final int index = mDrawingOrder\r
+                == DRAW_ORDER_REVERSE ? childCount - 1 - i : i;\r
+        final int result\r
+                = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex;\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * Set a separate OnPageChangeListener for internal use by the support library.\r
+     *\r
+     * @param listener Listener to set\r
+     * @return The old listener that was set, if any.\r
+     */\r
+    OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) {\r
+        OnPageChangeListener oldListener = mInternalPageChangeListener;\r
+        mInternalPageChangeListener = listener;\r
+        return oldListener;\r
+    }\r
+\r
+    /**\r
+     * Returns the number of pages that will be retained to either side of the\r
+     * current page in the view hierarchy in an idle state. Defaults to 1.\r
+     *\r
+     * @return How many pages will be kept offscreen on either side\r
+     * @see #setOffscreenPageLimit(int)\r
+     */\r
+    public int getOffscreenPageLimit() {\r
+        return mOffscreenPageLimit;\r
+    }\r
+\r
+    /**\r
+     * Set the number of pages that should be\r
+     * retained to either side of the\r
+     * current page in the view hierarchy\r
+     * in an idle state. Pages beyond this\r
+     * limit will be recreated from the adapter when needed.\r
+     * <p>\r
+     * <p>This is offered as an optimization.\r
+     * If you know in advance the number\r
+     * of pages you will need to support or\r
+     * have lazy-loading mechanisms in place\r
+     * on your pages, tweaking this setting\r
+     * can have benefits in perceived smoothness\r
+     * of paging animations and interaction.\r
+     * If you have a small number of pages (3-4)\r
+     * that you can keep active all at once,\r
+     * less time will be spent in layout for\r
+     * newly created view subtrees as the\r
+     * user pages back and forth.</p>\r
+     * <p>\r
+     * <p>You should keep this limit low,\r
+     * especially if your pages have complex layouts.\r
+     * This setting defaults to 1.</p>\r
+     *\r
+     * @param limit How many pages will be kept offscreen in an idle state.\r
+     */\r
+    public void setOffscreenPageLimit(int limit) {\r
+        if (limit < DEFAULT_OFFSCREEN_PAGES) {\r
+            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +\r
+                    DEFAULT_OFFSCREEN_PAGES);\r
+            limit = DEFAULT_OFFSCREEN_PAGES;\r
+        }\r
+        if (limit != mOffscreenPageLimit) {\r
+            mOffscreenPageLimit = limit;\r
+            populate();\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Set the margin between pages.\r
+     *\r
+     * @param marginPixels Distance between adjacent pages in pixels\r
+     * @see #getPageMargin()\r
+     * @see #setPageMarginDrawable(Drawable)\r
+     * @see #setPageMarginDrawable(int)\r
+     */\r
+    public void setPageMargin(int marginPixels) {\r
+        final int oldMargin = mPageMargin;\r
+        mPageMargin = marginPixels;\r
+\r
+        if (isHorizontal()) {\r
+            int width = getWidth();\r
+            recomputeScrollPosition(width, width, marginPixels, oldMargin, 0, 0);\r
+        } else {\r
+            int height = getHeight();\r
+            recomputeScrollPosition(0, 0, marginPixels, oldMargin, height, height);\r
+        }\r
+\r
+        requestLayout();\r
+    }\r
+\r
+    /**\r
+     * Return the margin between pages.\r
+     *\r
+     * @return The size of the margin in pixels\r
+     */\r
+    public int getPageMargin() {\r
+        return mPageMargin;\r
+    }\r
+\r
+    /**\r
+     * Set a drawable that will be used to fill the margin between pages.\r
+     *\r
+     * @param d Drawable to display between pages\r
+     */\r
+    public void setPageMarginDrawable(Drawable d) {\r
+        mMarginDrawable = d;\r
+        if (d != null) refreshDrawableState();\r
+        setWillNotDraw(d == null);\r
+        invalidate();\r
+    }\r
+\r
+    /**\r
+     * Set a drawable that will be used to fill the margin between pages.\r
+     *\r
+     * @param resId Resource ID of a drawable to display between pages\r
+     */\r
+    public void setPageMarginDrawable(@DrawableRes int resId) {\r
+        setPageMarginDrawable(getContext().getResources().getDrawable(resId));\r
+    }\r
+\r
+    @Override\r
+    protected boolean verifyDrawable(Drawable who) {\r
+        return super.verifyDrawable(who) || who == mMarginDrawable;\r
+    }\r
+\r
+    @Override\r
+    protected void drawableStateChanged() {\r
+        super.drawableStateChanged();\r
+        final Drawable d = mMarginDrawable;\r
+        if (d != null && d.isStateful()) {\r
+            d.setState(getDrawableState());\r
+        }\r
+    }\r
+\r
+    // We want the duration of the page snap animation to be influenced by the distance that\r
+    // the screen has to travel, however, we don't want this duration to be effected in a\r
+    // purely linear fashion. Instead, we use this method to moderate the effect that the distance\r
+    // of travel has on the overall snap duration.\r
+    float distanceInfluenceForSnapDuration(float f) {\r
+        f -= 0.5f; // center the values about 0.\r
+        f *= 0.3f * Math.PI / 2.0f;\r
+        return (float) Math.sin(f);\r
+    }\r
+\r
+    /**\r
+     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.\r
+     *\r
+     * @param x the number of pixels to scroll by on the X axis\r
+     * @param y the number of pixels to scroll by on the Y axis\r
+     */\r
+    void smoothScrollTo(int x, int y) {\r
+        smoothScrollTo(x, y, 0);\r
+    }\r
+\r
+    /**\r
+     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.\r
+     *\r
+     * @param x        the number of pixels to scroll by on the X axis\r
+     * @param y        the number of pixels to scroll by on the Y axis\r
+     * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)\r
+     */\r
+    void smoothScrollTo(int x, int y, int velocity) {\r
+        if (getChildCount() == 0) {\r
+            // Nothing to do.\r
+            setScrollingCacheEnabled(false);\r
+            return;\r
+        }\r
+\r
+        int sx;\r
+        if (isHorizontal()) {\r
+            boolean wasScrolling = (mScroller != null) && !mScroller.isFinished();\r
+            if (wasScrolling) {\r
+                // We're in the middle of a previously initiated scrolling. Check to see\r
+                // whether that scrolling has actually started (if we always call getStartX\r
+                // we can get a stale value from the scroller if it hadn't yet had its first\r
+                // computeScrollOffset call) to decide what is the current scrolling position.\r
+                sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX();\r
+                // And abort the current scrolling.\r
+                mScroller.abortAnimation();\r
+                setScrollingCacheEnabled(false);\r
+            } else {\r
+                sx = getScrollX();\r
+            }\r
+        } else {\r
+            sx = getScrollX();\r
+        }\r
+        int sy = getScrollY();\r
+        int dx = x - sx;\r
+        int dy = y - sy;\r
+        if (dx == 0 && dy == 0) {\r
+            completeScroll(false);\r
+            populate();\r
+            setScrollState(SCROLL_STATE_IDLE);\r
+            return;\r
+        }\r
+\r
+        setScrollingCacheEnabled(true);\r
+        setScrollState(SCROLL_STATE_SETTLING);\r
+        int duration = 0;\r
+        if (isHorizontal()) {\r
+            final int width = getClientWidth();\r
+            final int halfWidth = width / 2;\r
+            final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);\r
+            final float distance = halfWidth + halfWidth *\r
+                    distanceInfluenceForSnapDuration(distanceRatio);\r
+            velocity = Math.abs(velocity);\r
+            if (velocity > 0) {\r
+                duration = 4 * Math.round(1000 * Math.abs(distance / velocity));\r
+            } else {\r
+                final float pageWidth = width * mAdapter.getPageWidth(mCurItem);\r
+                final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);\r
+                duration = (int) ((pageDelta + 1) * 100);\r
+            }\r
+        } else {\r
+            final int height = getClientHeight();\r
+            final int halfHeight = height / 2;\r
+            final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / height);\r
+            final float distance = halfHeight + halfHeight *\r
+                    distanceInfluenceForSnapDuration(distanceRatio);\r
+\r
+            duration = 0;\r
+            velocity = Math.abs(velocity);\r
+            if (velocity > 0) {\r
+                duration = 4 * Math.round(1000 * Math.abs(distance / velocity));\r
+            } else {\r
+                final float pageHeight = height * mAdapter.getPageWidth(mCurItem);\r
+                final float pageDelta = (float) Math.abs(dx) / (pageHeight + mPageMargin);\r
+                duration = (int) ((pageDelta + 1) * 100);\r
+            }\r
+        }\r
+        duration = Math.min(duration, MAX_SETTLE_DURATION);\r
+\r
+        // Reset the "scroll started" flag. It will be flipped to true in all places\r
+        // where we call computeScrollOffset().\r
+        if (isHorizontal()) {\r
+            mIsScrollStarted = false;\r
+        }\r
+        mScroller.startScroll(sx, sy, dx, dy, duration);\r
+        ViewCompat.postInvalidateOnAnimation(this);\r
+    }\r
+\r
+    private boolean isHorizontal() {\r
+        return mDirection.equalsIgnoreCase(Direction.HORIZONTAL.name());\r
+    }\r
+\r
+    ItemInfo addNewItem(int position, int index) {\r
+        ItemInfo ii = new ItemInfo();\r
+        ii.position = position;\r
+        ii.object = mAdapter.instantiateItem(this, position);\r
+        if (isHorizontal()) {\r
+            ii.widthFactor = mAdapter.getPageWidth(position);\r
+        } else {\r
+            ii.heightFactor = mAdapter.getPageWidth(position);\r
+        }\r
+        if (index < 0 || index >= mItems.size()) {\r
+            mItems.add(ii);\r
+        } else {\r
+            mItems.add(index, ii);\r
+        }\r
+        return ii;\r
+    }\r
+\r
+    void dataSetChanged() {\r
+        // This method only gets called if our observer is attached, so mAdapter is non-null.\r
+\r
+        final int adapterCount = mAdapter.getCount();\r
+        mExpectedAdapterCount = adapterCount;\r
+        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&\r
+                mItems.size() < adapterCount;\r
+        int newCurrItem = mCurItem;\r
+\r
+        boolean isUpdating = false;\r
+        for (int i = 0; i < mItems.size(); i++) {\r
+            final ItemInfo ii = mItems.get(i);\r
+            final int newPos = mAdapter.getItemPosition(ii.object);\r
+\r
+            if (newPos == PagerAdapter.POSITION_UNCHANGED) {\r
+                continue;\r
+            }\r
+\r
+            if (newPos == PagerAdapter.POSITION_NONE) {\r
+                mItems.remove(i);\r
+                i--;\r
+\r
+                if (!isUpdating) {\r
+                    mAdapter.startUpdate(this);\r
+                    isUpdating = true;\r
+                }\r
+\r
+                mAdapter.destroyItem(this, ii.position, ii.object);\r
+                needPopulate = true;\r
+\r
+                if (mCurItem == ii.position) {\r
+                    // Keep the current item in the valid range\r
+                    newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));\r
+                    needPopulate = true;\r
+                }\r
+                continue;\r
+            }\r
+\r
+            if (ii.position != newPos) {\r
+                if (ii.position == mCurItem) {\r
+                    // Our current item changed position. Follow it.\r
+                    newCurrItem = newPos;\r
+                }\r
+\r
+                ii.position = newPos;\r
+                needPopulate = true;\r
+            }\r
+        }\r
+\r
+        if (isUpdating) {\r
+            mAdapter.finishUpdate(this);\r
+        }\r
+\r
+        Collections.sort(mItems, COMPARATOR);\r
+\r
+        if (needPopulate) {\r
+            // Reset our known page widths; populate will recompute them.\r
+            final int childCount = getChildCount();\r
+            for (int i = 0; i < childCount; i++) {\r
+                final View child = getChildAt(i);\r
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                if (!lp.isDecor) {\r
+                    if (isHorizontal()) {\r
+                        lp.widthFactor = 0.f;\r
+                    } else {\r
+                        lp.heightFactor = 0.f;\r
+                    }\r
+                }\r
+            }\r
+\r
+            setCurrentItemInternal(newCurrItem, false, true);\r
+            requestLayout();\r
+        }\r
+    }\r
+\r
+    void populate() {\r
+        populate(mCurItem);\r
+    }\r
+\r
+    void populate(int newCurrentItem) {\r
+        ItemInfo oldCurInfo = null;\r
+        int focusDirection = View.FOCUS_FORWARD;\r
+        if (mCurItem != newCurrentItem) {\r
+            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_DOWN : View.FOCUS_UP;\r
+            oldCurInfo = infoForPosition(mCurItem);\r
+            mCurItem = newCurrentItem;\r
+        }\r
+\r
+        if (mAdapter == null) {\r
+            sortChildDrawingOrder();\r
+            return;\r
+        }\r
+\r
+        // Bail now if we are waiting to populate.  This is to hold off\r
+        // on creating views from the time the user releases their finger to\r
+        // fling to a new position until we have finished the scroll to\r
+        // that position, avoiding glitches from happening at that point.\r
+        if (mPopulatePending) {\r
+            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");\r
+            sortChildDrawingOrder();\r
+            return;\r
+        }\r
+\r
+        // Also, don't populate until we are attached to a window.  This is to\r
+        // avoid trying to populate before we have restored our view hierarchy\r
+        // state and conflicting with what is restored.\r
+        if (getWindowToken() == null) {\r
+            return;\r
+        }\r
+\r
+        mAdapter.startUpdate(this);\r
+\r
+        final int pageLimit = mOffscreenPageLimit;\r
+        final int startPos = Math.max(0, mCurItem - pageLimit);\r
+        final int N = mAdapter.getCount();\r
+        final int endPos = Math.min(N - 1, mCurItem + pageLimit);\r
+\r
+        if (N != mExpectedAdapterCount) {\r
+            String resName;\r
+            try {\r
+                resName = getResources().getResourceName(getId());\r
+            } catch (Resources.NotFoundException e) {\r
+                resName = Integer.toHexString(getId());\r
+            }\r
+            throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +\r
+                    " contents without calling PagerAdapter#notifyDataSetChanged!" +\r
+                    " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +\r
+                    " Pager id: " + resName +\r
+                    " Pager class: " + getClass() +\r
+                    " Problematic adapter: " + mAdapter.getClass());\r
+        }\r
+\r
+        // Locate the currently focused item or add it if needed.\r
+        int curIndex = -1;\r
+        ItemInfo curItem = null;\r
+        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {\r
+            final ItemInfo ii = mItems.get(curIndex);\r
+            if (ii.position >= mCurItem) {\r
+                if (ii.position == mCurItem) curItem = ii;\r
+                break;\r
+            }\r
+        }\r
+\r
+        if (curItem == null && N > 0) {\r
+            curItem = addNewItem(mCurItem, curIndex);\r
+        }\r
+\r
+        // Fill 3x the available width or up to the number of offscreen\r
+        // pages requested to either side, whichever is larger.\r
+        // If we have no current item we have no work to do.\r
+        if (curItem != null) {\r
+            if (isHorizontal()) {\r
+                float extraWidthLeft = 0.f;\r
+                int itemIndex = curIndex - 1;\r
+                ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;\r
+                final int clientWidth = getClientWidth();\r
+                final float leftWidthNeeded = clientWidth <= 0 ? 0 :\r
+                        2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;\r
+                for (int pos = mCurItem - 1; pos >= 0; pos--) {\r
+                    if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {\r
+                        if (ii == null) {\r
+                            break;\r
+                        }\r
+                        if (pos == ii.position && !ii.scrolling) {\r
+                            mItems.remove(itemIndex);\r
+                            mAdapter.destroyItem(this, pos, ii.object);\r
+                            if (DEBUG) {\r
+                                Log.i(TAG, logDestroyItem(pos, ((View) ii.object)));\r
+                            }\r
+                            itemIndex--;\r
+                            curIndex--;\r
+                            ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;\r
+                        }\r
+                    } else if (ii != null && pos == ii.position) {\r
+                        extraWidthLeft += ii.widthFactor;\r
+                        itemIndex--;\r
+                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;\r
+                    } else {\r
+                        ii = addNewItem(pos, itemIndex + 1);\r
+                        extraWidthLeft += ii.widthFactor;\r
+                        curIndex++;\r
+                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;\r
+                    }\r
+                }\r
+\r
+                float extraWidthRight = curItem.widthFactor;\r
+                itemIndex = curIndex + 1;\r
+                if (extraWidthRight < 2.f) {\r
+                    ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;\r
+                    final float rightWidthNeeded = clientWidth <= 0 ? 0 :\r
+                            (float) getPaddingRight() / (float) clientWidth + 2.f;\r
+                    for (int pos = mCurItem + 1; pos < N; pos++) {\r
+                        if (extraWidthRight >= rightWidthNeeded && pos > endPos) {\r
+                            if (ii == null) {\r
+                                break;\r
+                            }\r
+                            if (pos == ii.position && !ii.scrolling) {\r
+                                mItems.remove(itemIndex);\r
+                                mAdapter.destroyItem(this, pos, ii.object);\r
+                                if (DEBUG) {\r
+                                    Log.i(TAG, logDestroyItem(pos, ((View) ii.object)));\r
+                                }\r
+                                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;\r
+                            }\r
+                        } else if (ii != null && pos == ii.position) {\r
+                            extraWidthRight += ii.widthFactor;\r
+                            itemIndex++;\r
+                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;\r
+                        } else {\r
+                            ii = addNewItem(pos, itemIndex);\r
+                            itemIndex++;\r
+                            extraWidthRight += ii.widthFactor;\r
+                            ii = itemIndex < mItems.size()\r
+                                    ? mItems.get(itemIndex) : null;\r
+                        }\r
+                    }\r
+                }\r
+            } else {\r
+                float extraHeightTop = 0.f;\r
+                int itemIndex = curIndex - 1;\r
+                ItemInfo ii\r
+                        = itemIndex >= 0 ? mItems.get(itemIndex) : null;\r
+                final int clientHeight = getClientHeight();\r
+                final float topHeightNeeded = clientHeight <= 0 ? 0 :\r
+                        2.f - curItem.heightFactor\r
+                                + (float) getPaddingLeft() / (float) clientHeight;\r
+                for (int pos = mCurItem - 1; pos >= 0; pos--) {\r
+                    if (extraHeightTop >= topHeightNeeded && pos < startPos) {\r
+                        if (ii == null) {\r
+                            break;\r
+                        }\r
+                        if (pos == ii.position && !ii.scrolling) {\r
+                            mItems.remove(itemIndex);\r
+                            mAdapter.destroyItem(this, pos, ii.object);\r
+                            if (DEBUG) {\r
+                                Log.i(TAG, logDestroyItem(pos, ((View) ii.object)));\r
+                            }\r
+                            itemIndex--;\r
+                            curIndex--;\r
+                            ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;\r
+                        }\r
+                    } else if (ii != null && pos == ii.position) {\r
+                        extraHeightTop += ii.heightFactor;\r
+                        itemIndex--;\r
+                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;\r
+                    } else {\r
+                        ii = addNewItem(pos, itemIndex + 1);\r
+                        extraHeightTop += ii.heightFactor;\r
+                        curIndex++;\r
+                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;\r
+                    }\r
+                }\r
+\r
+                float extraHeightBottom = curItem.heightFactor;\r
+                itemIndex = curIndex + 1;\r
+                if (extraHeightBottom < 2.f) {\r
+                    ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;\r
+                    final float bottomHeightNeeded = clientHeight <= 0 ? 0 :\r
+                            (float) getPaddingRight() / (float) clientHeight + 2.f;\r
+                    for (int pos = mCurItem + 1; pos < N; pos++) {\r
+                        if (extraHeightBottom >= bottomHeightNeeded && pos > endPos) {\r
+                            if (ii == null) {\r
+                                break;\r
+                            }\r
+                            if (pos == ii.position && !ii.scrolling) {\r
+                                mItems.remove(itemIndex);\r
+                                mAdapter.destroyItem(this, pos, ii.object);\r
+                                if (DEBUG) {\r
+                                    Log.i(TAG, logDestroyItem(pos, ((View) ii.object)));\r
+                                }\r
+                                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;\r
+                            }\r
+                        } else if (ii != null && pos == ii.position) {\r
+                            extraHeightBottom += ii.heightFactor;\r
+                            itemIndex++;\r
+                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;\r
+                        } else {\r
+                            ii = addNewItem(pos, itemIndex);\r
+                            itemIndex++;\r
+                            extraHeightBottom += ii.heightFactor;\r
+                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;\r
+                        }\r
+                    }\r
+                }\r
+            }\r
+\r
+            calculatePageOffsets(curItem, curIndex, oldCurInfo);\r
+        }\r
+\r
+        if (DEBUG) {\r
+            Log.i(TAG, "Current page list:");\r
+            for (int i = 0; i < mItems.size(); i++) {\r
+                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);\r
+            }\r
+        }\r
+\r
+        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);\r
+\r
+        mAdapter.finishUpdate(this);\r
+\r
+        // Check width measurement of current pages and drawing sort order.\r
+        // Update LayoutParams as needed.\r
+        final int childCount = getChildCount();\r
+        if (isHorizontal()) {\r
+            for (int i = 0; i < childCount; i++) {\r
+                final View child = getChildAt(i);\r
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                lp.childIndex = i;\r
+                if (!lp.isDecor && lp.widthFactor == 0.f) {\r
+                    // 0 means requery the adapter for this, it doesn't have a valid width.\r
+                    final ItemInfo ii = infoForChild(child);\r
+                    if (ii != null) {\r
+                        lp.widthFactor = ii.widthFactor;\r
+                        lp.position = ii.position;\r
+                    }\r
+                }\r
+            }\r
+            sortChildDrawingOrder();\r
+\r
+            if (hasFocus()) {\r
+                View currentFocused = findFocus();\r
+                ItemInfo ii\r
+                        = currentFocused != null ? infoForAnyChild(currentFocused) : null;\r
+                if (ii == null || ii.position != mCurItem) {\r
+                    for (int i = 0; i < getChildCount(); i++) {\r
+                        View child = getChildAt(i);\r
+                        ii = infoForChild(child);\r
+                        if (ii != null\r
+                                && ii.position == mCurItem &&\r
+                                child.requestFocus(View.FOCUS_FORWARD)) {\r
+                            break;\r
+                        }\r
+                    }\r
+                }\r
+            }\r
+        } else {\r
+            for (int i = 0; i < childCount; i++) {\r
+                final View child = getChildAt(i);\r
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                lp.childIndex = i;\r
+                if (!lp.isDecor && lp.heightFactor == 0.f) {\r
+                    final ItemInfo ii = infoForChild(child);\r
+                    if (ii != null) {\r
+                        lp.heightFactor = ii.heightFactor;\r
+                        lp.position = ii.position;\r
+                    }\r
+                }\r
+            }\r
+            sortChildDrawingOrder();\r
+\r
+            if (hasFocus()) {\r
+                View currentFocused = findFocus();\r
+                ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;\r
+                if (ii == null || ii.position != mCurItem) {\r
+                    for (int i = 0; i < getChildCount(); i++) {\r
+                        View child = getChildAt(i);\r
+                        ii = infoForChild(child);\r
+                        if (ii != null && ii.position == mCurItem\r
+                                && child.requestFocus(focusDirection)) {\r
+//                        if (child.requestFocus(focusDirection)) {\r
+                            break;\r
+                            // }\r
+                        }\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    private void sortChildDrawingOrder() {\r
+        if (mDrawingOrder != DRAW_ORDER_DEFAULT) {\r
+            if (mDrawingOrderedChildren == null) {\r
+                mDrawingOrderedChildren = new ArrayList<View>();\r
+            } else {\r
+                mDrawingOrderedChildren.clear();\r
+            }\r
+            final int childCount = getChildCount();\r
+            for (int i = 0; i < childCount; i++) {\r
+                final View child = getChildAt(i);\r
+                mDrawingOrderedChildren.add(child);\r
+            }\r
+            Collections.sort(mDrawingOrderedChildren, sPositionComparator);\r
+        }\r
+    }\r
+\r
+    private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {\r
+        final int N = mAdapter.getCount();\r
+        if (isHorizontal()) {\r
+            final int width = getClientWidth();\r
+            final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;\r
+            // Fix up offsets for later layout.\r
+            if (oldCurInfo != null) {\r
+                final int oldCurPosition = oldCurInfo.position;\r
+                // Base offsets off of oldCurInfo.\r
+                if (oldCurPosition < curItem.position) {\r
+                    int itemIndex = 0;\r
+                    ItemInfo ii = null;\r
+                    float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;\r
+                    for (int pos = oldCurPosition + 1;\r
+                             pos <= curItem.position && itemIndex < mItems.size(); pos++) {\r
+                        ii = mItems.get(itemIndex);\r
+                        while (pos > ii.position && itemIndex < mItems.size() - 1) {\r
+                            itemIndex++;\r
+                            ii = mItems.get(itemIndex);\r
+                        }\r
+                        while (pos < ii.position) {\r
+                            // We don't have an item populated for this,\r
+                            // ask the adapter for an offset.\r
+                            offset += mAdapter.getPageWidth(pos) + marginOffset;\r
+                            pos++;\r
+                        }\r
+                        ii.offset = offset;\r
+                        offset += ii.widthFactor + marginOffset;\r
+                    }\r
+                } else if (oldCurPosition > curItem.position) {\r
+                    int itemIndex = mItems.size() - 1;\r
+                    ItemInfo ii = null;\r
+                    float offset = oldCurInfo.offset;\r
+                    for (int pos = oldCurPosition - 1;\r
+                            pos >= curItem.position && itemIndex >= 0; pos--) {\r
+                        ii = mItems.get(itemIndex);\r
+                        while (pos < ii.position && itemIndex > 0) {\r
+                            itemIndex--;\r
+                            ii = mItems.get(itemIndex);\r
+                        }\r
+                        while (pos > ii.position) {\r
+                            // We don't have an item populated for this,\r
+                            // ask the adapter for an offset.\r
+                            offset -= mAdapter.getPageWidth(pos) + marginOffset;\r
+                            pos--;\r
+                        }\r
+                        offset -= ii.widthFactor + marginOffset;\r
+                        ii.offset = offset;\r
+                    }\r
+                }\r
+            }\r
+\r
+            // Base all offsets off of curItem.\r
+            final int itemCount = mItems.size();\r
+            float offset = curItem.offset;\r
+            int pos = curItem.position - 1;\r
+            mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;\r
+            mLastOffset = curItem.position == N - 1 ?\r
+                    curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;\r
+            // Previous pages\r
+            for (int i = curIndex - 1; i >= 0; i--, pos--) {\r
+                final ItemInfo ii = mItems.get(i);\r
+                while (pos > ii.position) {\r
+                    offset -= mAdapter.getPageWidth(pos--) + marginOffset;\r
+                }\r
+                offset -= ii.widthFactor + marginOffset;\r
+                ii.offset = offset;\r
+                if (ii.position == 0) mFirstOffset = offset;\r
+            }\r
+            offset = curItem.offset + curItem.widthFactor + marginOffset;\r
+            pos = curItem.position + 1;\r
+            // Next pages\r
+            for (int i = curIndex + 1; i < itemCount; i++, pos++) {\r
+                final ItemInfo ii = mItems.get(i);\r
+                while (pos < ii.position) {\r
+                    offset += mAdapter.getPageWidth(pos++) + marginOffset;\r
+                }\r
+                if (ii.position == N - 1) {\r
+                    mLastOffset = offset + ii.widthFactor - 1;\r
+                }\r
+                ii.offset = offset;\r
+                offset += ii.widthFactor + marginOffset;\r
+            }\r
+        } else {\r
+            final int height = getClientHeight();\r
+            final float marginOffset = height > 0 ? (float) mPageMargin / height : 0;\r
+            // Fix up offsets for later layout.\r
+            if (oldCurInfo != null) {\r
+                final int oldCurPosition = oldCurInfo.position;\r
+                // Base offsets off of oldCurInfo.\r
+                if (oldCurPosition < curItem.position) {\r
+                    int itemIndex = 0;\r
+                    ItemInfo ii = null;\r
+                    float offset = oldCurInfo.offset + oldCurInfo.heightFactor + marginOffset;\r
+                    for (int pos = oldCurPosition + 1;\r
+                            pos <= curItem.position && itemIndex < mItems.size(); pos++) {\r
+                        ii = mItems.get(itemIndex);\r
+                        while (pos > ii.position && itemIndex < mItems.size() - 1) {\r
+                            itemIndex++;\r
+                            ii = mItems.get(itemIndex);\r
+                        }\r
+                        while (pos < ii.position) {\r
+                            // We don't have an item populated for this,\r
+                            // ask the adapter for an offset.\r
+                            offset += mAdapter.getPageWidth(pos) + marginOffset;\r
+                            pos++;\r
+                        }\r
+                        ii.offset = offset;\r
+                        offset += ii.heightFactor + marginOffset;\r
+                    }\r
+                } else if (oldCurPosition > curItem.position) {\r
+                    int itemIndex = mItems.size() - 1;\r
+                    ItemInfo ii = null;\r
+                    float offset = oldCurInfo.offset;\r
+                    for (int pos = oldCurPosition - 1;\r
+                            pos >= curItem.position && itemIndex >= 0;\r
+                                pos--) {\r
+                        ii = mItems.get(itemIndex);\r
+                        while (pos < ii.position && itemIndex > 0) {\r
+                            itemIndex--;\r
+                            ii = mItems.get(itemIndex);\r
+                        }\r
+                        while (pos > ii.position) {\r
+                            // We don't have an item populated for this,\r
+                            // ask the adapter for an offset.\r
+                            offset -= mAdapter.getPageWidth(pos) + marginOffset;\r
+                            pos--;\r
+                        }\r
+                        offset -= ii.heightFactor + marginOffset;\r
+                        ii.offset = offset;\r
+                    }\r
+                }\r
+            }\r
+\r
+            // Base all offsets off of curItem.\r
+            final int itemCount = mItems.size();\r
+            float offset = curItem.offset;\r
+            int pos = curItem.position - 1;\r
+            mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;\r
+            mLastOffset = curItem.position == N - 1 ?\r
+                    curItem.offset + curItem.heightFactor - 1 : Float.MAX_VALUE;\r
+            // Previous pages\r
+            for (int i = curIndex - 1; i >= 0; i--, pos--) {\r
+                final ItemInfo ii = mItems.get(i);\r
+                while (pos > ii.position) {\r
+                    offset -= mAdapter.getPageWidth(pos--) + marginOffset;\r
+                }\r
+                offset -= ii.heightFactor + marginOffset;\r
+                ii.offset = offset;\r
+                if (ii.position == 0) mFirstOffset = offset;\r
+            }\r
+            offset = curItem.offset + curItem.heightFactor + marginOffset;\r
+            pos = curItem.position + 1;\r
+            // Next pages\r
+            for (int i = curIndex + 1; i < itemCount; i++, pos++) {\r
+                final ItemInfo ii = mItems.get(i);\r
+                while (pos < ii.position) {\r
+                    offset += mAdapter.getPageWidth(pos++) + marginOffset;\r
+                }\r
+                if (ii.position == N - 1) {\r
+                    mLastOffset = offset + ii.heightFactor - 1;\r
+                }\r
+                ii.offset = offset;\r
+                offset += ii.heightFactor + marginOffset;\r
+            }\r
+        }\r
+\r
+        mNeedCalculatePageOffsets = false;\r
+    }\r
+\r
+    /**\r
+     * This is the persistent state that is saved by ViewPager.  Only needed\r
+     * if you are creating a sublass of ViewPager that must save its own\r
+     * state, in which case it should implement a subclass of this which\r
+     * contains that state.\r
+     */\r
+    public static class SavedState extends BaseSavedState {\r
+        int position;\r
+        Parcelable adapterState;\r
+        ClassLoader loader;\r
+\r
+        public SavedState(Parcelable superState) {\r
+            super(superState);\r
+        }\r
+\r
+        @Override\r
+        public void writeToParcel(Parcel out, int flags) {\r
+            super.writeToParcel(out, flags);\r
+            out.writeInt(position);\r
+            out.writeParcelable(adapterState, flags);\r
+        }\r
+\r
+        @Override\r
+        public String toString() {\r
+            return "FragmentPager.SavedState{"\r
+                    + Integer.toHexString(System.identityHashCode(this))\r
+                    + " position=" + position + "}";\r
+        }\r
+\r
+        public static final Parcelable.Creator<SavedState> CREATOR\r
+                = ParcelableCompat\r
+                .newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {\r
+                    @Override\r
+                    public SavedState\r
+                            createFromParcel(Parcel in, ClassLoader loader) {\r
+                                return new SavedState(in, loader);\r
+                            }\r
+\r
+                            @Override\r
+                            public SavedState[] newArray(int size) {\r
+                                return new SavedState[size];\r
+                            }\r
+                });\r
+\r
+        SavedState(Parcel in, ClassLoader loader) {\r
+            super(in);\r
+            if (loader == null) {\r
+                loader = getClass().getClassLoader();\r
+            }\r
+            position = in.readInt();\r
+            adapterState = in.readParcelable(loader);\r
+            this.loader = loader;\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public Parcelable onSaveInstanceState() {\r
+        Parcelable superState = super.onSaveInstanceState();\r
+        SavedState ss = new SavedState(superState);\r
+        ss.position = mCurItem;\r
+        if (mAdapter != null) {\r
+            ss.adapterState = mAdapter.saveState();\r
+        }\r
+        return ss;\r
+    }\r
+\r
+    @Override\r
+    public void onRestoreInstanceState(Parcelable state) {\r
+        if (!(state instanceof SavedState)) {\r
+            super.onRestoreInstanceState(state);\r
+            return;\r
+        }\r
+\r
+        SavedState ss = (SavedState) state;\r
+        super.onRestoreInstanceState(ss.getSuperState());\r
+\r
+        if (mAdapter != null) {\r
+            mAdapter.restoreState(ss.adapterState, ss.loader);\r
+            setCurrentItemInternal(ss.position, false, true);\r
+        } else {\r
+            mRestoredCurItem = ss.position;\r
+            mRestoredAdapterState = ss.adapterState;\r
+            mRestoredClassLoader = ss.loader;\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {\r
+        if (!checkLayoutParams(params)) {\r
+            params = generateLayoutParams(params);\r
+        }\r
+        final LayoutParams lp = (LayoutParams) params;\r
+        lp.isDecor |= child instanceof Decor;\r
+        if (mInLayout) {\r
+            if (lp != null && lp.isDecor) {\r
+                throw new IllegalStateException("Cannot add pager decor view during layout");\r
+            }\r
+            lp.needsMeasure = true;\r
+            addViewInLayout(child, index, params);\r
+        } else {\r
+            super.addView(child, index, params);\r
+        }\r
+\r
+        if (USE_CACHE) {\r
+            if (child.getVisibility() != GONE) {\r
+                child.setDrawingCacheEnabled(mScrollingCacheEnabled);\r
+            } else {\r
+                child.setDrawingCacheEnabled(false);\r
+            }\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public void removeView(View view) {\r
+        if (mInLayout) {\r
+            removeViewInLayout(view);\r
+        } else {\r
+            super.removeView(view);\r
+        }\r
+    }\r
+\r
+    ItemInfo infoForChild(View child) {\r
+        for (int i = 0; i < mItems.size(); i++) {\r
+            ItemInfo ii = mItems.get(i);\r
+            if (mAdapter.isViewFromObject(child, ii.object)) {\r
+                return ii;\r
+            }\r
+        }\r
+        return null;\r
+    }\r
+\r
+    ItemInfo infoForAnyChild(View child) {\r
+        ViewParent parent;\r
+        while ((parent = child.getParent()) != this) {\r
+            if (parent == null || !(parent instanceof View)) {\r
+                return null;\r
+            }\r
+            child = (View) parent;\r
+        }\r
+        return infoForChild(child);\r
+    }\r
+\r
+    ItemInfo infoForPosition(int position) {\r
+        for (int i = 0; i < mItems.size(); i++) {\r
+            ItemInfo ii = mItems.get(i);\r
+            if (ii.position == position) {\r
+                return ii;\r
+            }\r
+        }\r
+        return null;\r
+    }\r
+\r
+    @Override\r
+    protected void onAttachedToWindow() {\r
+        super.onAttachedToWindow();\r
+        mFirstLayout = true;\r
+    }\r
+\r
+    @Override\r
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\r
+        // For simple implementation, our internal size is always 0.\r
+        // We depend on the container to specify the layout size of\r
+        // our view.  We can't really know what it is since we will be\r
+        // adding and removing different arbitrary views and do not\r
+        // want the layout to change as this happens.\r
+        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),\r
+                getDefaultSize(0, heightMeasureSpec));\r
+\r
+        int childWidthSize = 0;\r
+        int childHeightSize = 0;\r
+        if (isHorizontal()) {\r
+            int measuredWidth = getMeasuredWidth();\r
+            int maxGutterSize = measuredWidth / 10;\r
+            mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);\r
+\r
+            // Children are just made to fill our space.\r
+            childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();\r
+            childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();\r
+        } else {\r
+            int measuredHeight = getMeasuredHeight();\r
+            int maxGutterSize = measuredHeight / 10;\r
+            mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);\r
+\r
+            // Children are just made to fill our space.\r
+            childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();\r
+            childHeightSize = measuredHeight - getPaddingTop() - getPaddingBottom();\r
+        }\r
+\r
+        /*\r
+         * Make sure all children have been properly measured. Decor views first.\r
+         * Right now we cheat and make this less complicated by assuming decor\r
+         * views won't intersect. We will pin to edges based on gravity.\r
+         */\r
+        int size = getChildCount();\r
+        for (int i = 0; i < size; ++i) {\r
+            final View child = getChildAt(i);\r
+            if (child.getVisibility() != GONE) {\r
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                if (lp != null && lp.isDecor) {\r
+                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;\r
+                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;\r
+                    int widthMode = MeasureSpec.AT_MOST;\r
+                    int heightMode = MeasureSpec.AT_MOST;\r
+                    boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;\r
+                    boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;\r
+\r
+                    if (consumeVertical) {\r
+                        widthMode = MeasureSpec.EXACTLY;\r
+                    } else if (consumeHorizontal) {\r
+                        heightMode = MeasureSpec.EXACTLY;\r
+                    }\r
+\r
+                    int widthSize = childWidthSize;\r
+                    int heightSize = childHeightSize;\r
+                    if (lp.width != LayoutParams.WRAP_CONTENT) {\r
+                        widthMode = MeasureSpec.EXACTLY;\r
+                        if (lp.width != LayoutParams.FILL_PARENT) {\r
+                            widthSize = lp.width;\r
+                        }\r
+                    }\r
+                    if (lp.height != LayoutParams.WRAP_CONTENT) {\r
+                        heightMode = MeasureSpec.EXACTLY;\r
+                        if (lp.height != LayoutParams.FILL_PARENT) {\r
+                            heightSize = lp.height;\r
+                        }\r
+                    }\r
+                    final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);\r
+                    final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);\r
+                    child.measure(widthSpec, heightSpec);\r
+\r
+                    if (consumeVertical) {\r
+                        childHeightSize -= child.getMeasuredHeight();\r
+                    } else if (consumeHorizontal) {\r
+                        childWidthSize -= child.getMeasuredWidth();\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);\r
+        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);\r
+\r
+        // Make sure we have created all fragments that we need to have shown.\r
+        mInLayout = true;\r
+        populate();\r
+        mInLayout = false;\r
+\r
+        // Page views next.\r
+        size = getChildCount();\r
+        for (int i = 0; i < size; ++i) {\r
+            final View child = getChildAt(i);\r
+            if (child.getVisibility() != GONE) {\r
+                if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child\r
+                        + ": " + mChildWidthMeasureSpec);\r
+\r
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                if (lp == null || !lp.isDecor) {\r
+                    if (isHorizontal()) {\r
+                        int widthSpec = MeasureSpec.makeMeasureSpec(\r
+                                (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);\r
+                        child.measure(widthSpec, mChildHeightMeasureSpec);\r
+                    } else {\r
+                        int heightSpec = MeasureSpec.makeMeasureSpec(\r
+                                (int) (childHeightSize * lp.heightFactor), MeasureSpec.EXACTLY);\r
+                        child.measure(mChildWidthMeasureSpec, heightSpec);\r
+                    }\r
+                }\r
+\r
+            }\r
+        }\r
+    }\r
+\r
+    @Override\r
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {\r
+        super.onSizeChanged(w, h, oldw, oldh);\r
+\r
+        // Make sure scroll position is set correctly.\r
+\r
+        if (isHorizontal()) {\r
+            if (w != oldw) {\r
+                recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin, 0, 0);\r
+            }\r
+        } else {\r
+            if (h != oldh) {\r
+                recomputeScrollPosition(0, 0, mPageMargin, mPageMargin, h, oldh);\r
+            }\r
+        }\r
+    }\r
+\r
+\r
+    private void recomputeScrollPosition(int width, int oldWidth, int margin,\r
+                                                int oldMargin, int height, int oldHeight) {\r
+        if (isHorizontal()) {\r
+            if (oldWidth > 0 && !mItems.isEmpty()) {\r
+                if (!mScroller.isFinished()) {\r
+                    mScroller.setFinalX(\r
+                            getCurrentItem() * getClientWidth());\r
+                } else {\r
+                    final int widthWithMargin\r
+                            = width - getPaddingLeft() - getPaddingRight() + margin;\r
+                    final int oldWidthWithMargin\r
+                            = oldWidth - getPaddingLeft() - getPaddingRight()\r
+                                + oldMargin;\r
+                    final int xpos = getScrollX();\r
+                    final float pageOffset = (float) xpos / oldWidthWithMargin;\r
+                    final int newOffsetPixels = (int) (pageOffset * widthWithMargin);\r
+\r
+                    scrollTo(newOffsetPixels, getScrollY());\r
+                }\r
+            } else {\r
+                final ItemInfo ii = infoForPosition(mCurItem);\r
+                final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;\r
+                final int scrollPos = (int) (scrollOffset *\r
+                        (width - getPaddingLeft() - getPaddingRight()));\r
+                if (scrollPos != getScrollX()) {\r
+                    completeScroll(false);\r
+                    scrollTo(scrollPos, getScrollY());\r
+                }\r
+            }\r
+        } else {\r
+            final int heightWithMargin = height - getPaddingTop() - getPaddingBottom() + margin;\r
+            final int oldHeightWithMargin = oldHeight - getPaddingTop() - getPaddingBottom()\r
+                    + oldMargin;\r
+            final int ypos = getScrollY();\r
+            final float pageOffset = (float) ypos / oldHeightWithMargin;\r
+            final int newOffsetPixels = (int) (pageOffset * heightWithMargin);\r
+\r
+            scrollTo(getScrollX(), newOffsetPixels);\r
+            if (!mScroller.isFinished()) {\r
+                // We now return to your regularly scheduled scroll, already in progress.\r
+                final int newDuration = mScroller.getDuration() - mScroller.timePassed();\r
+                ItemInfo targetInfo = infoForPosition(mCurItem);\r
+                mScroller.startScroll(0, newOffsetPixels,\r
+                        0, (int) (targetInfo.offset * height), newDuration);\r
+            } else {\r
+                final ItemInfo ii = infoForPosition(mCurItem);\r
+                final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;\r
+                final int scrollPos = (int) (scrollOffset *\r
+                        (height - getPaddingTop() - getPaddingBottom()));\r
+                if (scrollPos != getScrollY()) {\r
+                    completeScroll(false);\r
+                    scrollTo(getScrollX(), scrollPos);\r
+                }\r
+            }\r
+        }\r
+\r
+    }\r
+\r
+    @Override\r
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {\r
+        final int count = getChildCount();\r
+        int width = r - l;\r
+        int height = b - t;\r
+        int paddingLeft = getPaddingLeft();\r
+        int paddingTop = getPaddingTop();\r
+        int paddingRight = getPaddingRight();\r
+        int paddingBottom = getPaddingBottom();\r
+        final int scrollX = getScrollX();\r
+        final int scrollY = getScrollY();\r
+\r
+        int decorCount = 0;\r
+\r
+        // First pass - decor views. We need to do this in two passes so that\r
+        // we have the proper offsets for non-decor views later.\r
+        for (int i = 0; i < count; i++) {\r
+            final View child = getChildAt(i);\r
+            if (child.getVisibility() != GONE) {\r
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                int childLeft = 0;\r
+                int childTop = 0;\r
+                if (lp.isDecor) {\r
+                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;\r
+                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;\r
+                    switch (hgrav) {\r
+                        default:\r
+                            childLeft = paddingLeft;\r
+                            break;\r
+                        case Gravity.LEFT:\r
+                            childLeft = paddingLeft;\r
+                            paddingLeft += child.getMeasuredWidth();\r
+                            break;\r
+                        case Gravity.CENTER_HORIZONTAL:\r
+                            childLeft = Math.max((width - child.getMeasuredWidth()) / 2,\r
+                                    paddingLeft);\r
+                            break;\r
+                        case Gravity.RIGHT:\r
+                            childLeft = width - paddingRight - child.getMeasuredWidth();\r
+                            paddingRight += child.getMeasuredWidth();\r
+                            break;\r
+                    }\r
+                    switch (vgrav) {\r
+                        default:\r
+                            childTop = paddingTop;\r
+                            break;\r
+                        case Gravity.TOP:\r
+                            childTop = paddingTop;\r
+                            paddingTop += child.getMeasuredHeight();\r
+                            break;\r
+                        case Gravity.CENTER_VERTICAL:\r
+                            childTop = Math.max((height - child.getMeasuredHeight()) / 2,\r
+                                    paddingTop);\r
+                            break;\r
+                        case Gravity.BOTTOM:\r
+                            childTop = height - paddingBottom - child.getMeasuredHeight();\r
+                            paddingBottom += child.getMeasuredHeight();\r
+                            break;\r
+                    }\r
+                    if (isHorizontal()) {\r
+                        childLeft += scrollX;\r
+                    } else {\r
+                        childTop += scrollY;\r
+                    }\r
+                    child.layout(childLeft, childTop,\r
+                            childLeft + child.getMeasuredWidth(),\r
+                            childTop + child.getMeasuredHeight());\r
+                    decorCount++;\r
+                }\r
+            }\r
+        }\r
+\r
+        if (isHorizontal()) {\r
+            final int childWidth = width - paddingLeft - paddingRight;\r
+            // Page views. Do this once we have the right padding offsets from above.\r
+            for (int i = 0; i < count; i++) {\r
+                final View child = getChildAt(i);\r
+                if (child.getVisibility() != GONE) {\r
+                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                    ItemInfo ii;\r
+                    if (!lp.isDecor && (ii = infoForChild(child)) != null) {\r
+                        int loff = (int) (childWidth * ii.offset);\r
+                        int childLeft = paddingLeft + loff;\r
+                        int childTop = paddingTop;\r
+                        if (lp.needsMeasure) {\r
+                            // This was added during layout and needs measurement.\r
+                            // Do it now that we know what we're working with.\r
+                            lp.needsMeasure = false;\r
+                            final int widthSpec = MeasureSpec.makeMeasureSpec(\r
+                                    (int) (childWidth * lp.widthFactor),\r
+                                    MeasureSpec.EXACTLY);\r
+                            final int heightSpec = MeasureSpec.makeMeasureSpec(\r
+                                    (int) (height - paddingTop - paddingBottom),\r
+                                    MeasureSpec.EXACTLY);\r
+                            child.measure(widthSpec, heightSpec);\r
+                        }\r
+                        if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object\r
+                                + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()\r
+                                + "x" + child.getMeasuredHeight());\r
+                        child.layout(childLeft, childTop,\r
+                                childLeft + child.getMeasuredWidth(),\r
+                                childTop + child.getMeasuredHeight());\r
+                    }\r
+                }\r
+            }\r
+            mTopPageBounds = paddingTop;\r
+            mBottomPageBounds = height - paddingBottom;\r
+        } else {\r
+            final int childHeight = height - paddingTop - paddingBottom;\r
+            // Page views. Do this once we have the right padding offsets from above.\r
+            for (int i = 0; i < count; i++) {\r
+                final View child = getChildAt(i);\r
+                if (child.getVisibility() != GONE) {\r
+                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                    ItemInfo ii;\r
+                    if (!lp.isDecor && (ii = infoForChild(child)) != null) {\r
+                        int toff = (int) (childHeight * ii.offset);\r
+                        int childLeft = paddingLeft;\r
+                        int childTop = paddingTop + toff;\r
+                        if (lp.needsMeasure) {\r
+                            // This was added during layout and needs measurement.\r
+                            // Do it now that we know what we're working with.\r
+                            lp.needsMeasure = false;\r
+                            final int widthSpec = MeasureSpec.makeMeasureSpec(\r
+                                    (int) (width - paddingLeft - paddingRight),\r
+                                    MeasureSpec.EXACTLY);\r
+                            final int heightSpec = MeasureSpec.makeMeasureSpec(\r
+                                    (int) (childHeight * lp.heightFactor),\r
+                                    MeasureSpec.EXACTLY);\r
+                            child.measure(widthSpec, heightSpec);\r
+                        }\r
+                        if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object\r
+                                + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()\r
+                                + "x" + child.getMeasuredHeight());\r
+                        child.layout(childLeft, childTop,\r
+                                childLeft + child.getMeasuredWidth(),\r
+                                childTop + child.getMeasuredHeight());\r
+                    }\r
+                }\r
+            }\r
+            mLeftPageBounds = paddingLeft;\r
+            mRightPageBounds = width - paddingRight;\r
+        }\r
+        mDecorChildCount = decorCount;\r
+\r
+        if (mFirstLayout) {\r
+            scrollToItem(mCurItem, false, 0, false);\r
+        }\r
+        mFirstLayout = false;\r
+    }\r
+\r
+    @Override\r
+    public void computeScroll() {\r
+        mIsScrollStarted = true;\r
+        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {\r
+            int oldX = getScrollX();\r
+            int oldY = getScrollY();\r
+            int x = mScroller.getCurrX();\r
+            int y = mScroller.getCurrY();\r
+\r
+            if (oldX != x || oldY != y) {\r
+                scrollTo(x, y);\r
+                if (isHorizontal()) {\r
+                    if (!pageScrolled(x, 0)) {\r
+                        mScroller.abortAnimation();\r
+                        scrollTo(0, y);\r
+                    }\r
+                } else {\r
+                    if (!pageScrolled(0, y)) {\r
+                        mScroller.abortAnimation();\r
+                        scrollTo(x, 0);\r
+                    }\r
+                }\r
+            }\r
+\r
+            // Keep on drawing until the animation has finished.\r
+            ViewCompat.postInvalidateOnAnimation(this);\r
+            return;\r
+        }\r
+\r
+        // Done with scroll, clean up state.\r
+        completeScroll(true);\r
+    }\r
+\r
+    private boolean pageScrolled(int xpos, int ypos) {\r
+        if (mItems.size() == 0) {\r
+            if (mFirstLayout) {\r
+                // If we haven't been laid out yet, we probably just haven't been populated yet.\r
+                // Let's skip this call since it doesn't make sense in this state\r
+                return false;\r
+            }\r
+            mCalledSuper = false;\r
+            onPageScrolled(0, 0, 0);\r
+            if (!mCalledSuper) {\r
+                throw new IllegalStateException(\r
+                        "onPageScrolled did not call superclass implementation");\r
+            }\r
+            return false;\r
+        }\r
+        final ItemInfo ii = infoForCurrentScrollPosition();\r
+        int currentPage = 0;\r
+        float pageOffset = 0;\r
+        int offsetPixels = 0;\r
+        if (isHorizontal()) {\r
+            int width = getClientWidth();\r
+            int widthWithMargin = width + mPageMargin;\r
+            float marginOffset = (float) mPageMargin / width;\r
+            currentPage = ii.position;\r
+            pageOffset = (((float) xpos / width) - ii.offset) /\r
+                    (ii.widthFactor + marginOffset);\r
+            offsetPixels = (int) (pageOffset * widthWithMargin);\r
+        } else {\r
+            int height = getClientHeight();\r
+            int heightWithMargin = height + mPageMargin;\r
+            float marginOffset = (float) mPageMargin / height;\r
+            currentPage = ii.position;\r
+            pageOffset = (((float) ypos / height) - ii.offset) /\r
+                    (ii.heightFactor + marginOffset);\r
+            offsetPixels = (int) (pageOffset * heightWithMargin);\r
+        }\r
+\r
+        mCalledSuper = false;\r
+        onPageScrolled(currentPage, pageOffset, offsetPixels);\r
+        if (!mCalledSuper) {\r
+            throw new IllegalStateException(\r
+                    "onPageScrolled did not call superclass implementation");\r
+        }\r
+        return true;\r
+    }\r
+\r
+    /**\r
+     * This method will be invoked when the current page is scrolled, either as part\r
+     * of a programmatically initiated smooth scroll or a user initiated touch scroll.\r
+     * If you override this method you must call through to the superclass implementation\r
+     * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled\r
+     * returns.\r
+     *\r
+     * @param position     Position index of the first page currently being displayed.\r
+     *                     Page position+1 will be visible if positionOffset is nonzero.\r
+     * @param offset       Value from [0, 1) indicating the offset from the page at position.\r
+     * @param offsetPixels Value in pixels indicating the offset from position.\r
+     */\r
+    @CallSuper\r
+    protected void onPageScrolled(int position, float offset, int offsetPixels) {\r
+        // Offset any decor views if needed - keep them on-screen at all times.\r
+        if (isHorizontal()) {\r
+            if (mDecorChildCount > 0) {\r
+                final int scrollX = getScrollX();\r
+                int paddingLeft = getPaddingLeft();\r
+                int paddingRight = getPaddingRight();\r
+                final int width = getWidth();\r
+                final int childCount = getChildCount();\r
+                for (int i = 0; i < childCount; i++) {\r
+                    final View child = getChildAt(i);\r
+                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                    if (!lp.isDecor) continue;\r
+\r
+                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;\r
+                    int childLeft = 0;\r
+                    switch (hgrav) {\r
+                        default:\r
+                            childLeft = paddingLeft;\r
+                            break;\r
+                        case Gravity.LEFT:\r
+                            childLeft = paddingLeft;\r
+                            paddingLeft += child.getWidth();\r
+                            break;\r
+                        case Gravity.CENTER_HORIZONTAL:\r
+                            childLeft = Math.max((width - child.getMeasuredWidth()) / 2,\r
+                                    paddingLeft);\r
+                            break;\r
+                        case Gravity.RIGHT:\r
+                            childLeft = width - paddingRight - child.getMeasuredWidth();\r
+                            paddingRight += child.getMeasuredWidth();\r
+                            break;\r
+                    }\r
+                    childLeft += scrollX;\r
+\r
+                    final int childOffset = childLeft - child.getLeft();\r
+                    if (childOffset != 0) {\r
+                        child.offsetLeftAndRight(childOffset);\r
+                    }\r
+                }\r
+            }\r
+\r
+            dispatchOnPageScrolled(position, offset, offsetPixels);\r
+\r
+            if (mPageTransformer != null) {\r
+                final int scrollX = getScrollX();\r
+                final int childCount = getChildCount();\r
+                for (int i = 0; i < childCount; i++) {\r
+                    final View child = getChildAt(i);\r
+                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+\r
+                    if (lp.isDecor) continue;\r
+                    final float transformPos\r
+                            = (float) (child.getLeft() - scrollX) / getClientWidth();\r
+                    mPageTransformer.transformPage(child, transformPos);\r
+                }\r
+            }\r
+        } else {\r
+            if (mDecorChildCount > 0) {\r
+                final int scrollY = getScrollY();\r
+                int paddingTop = getPaddingTop();\r
+                int paddingBottom = getPaddingBottom();\r
+                final int height = getHeight();\r
+                final int childCount = getChildCount();\r
+                for (int i = 0; i < childCount; i++) {\r
+                    final View child = getChildAt(i);\r
+                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+                    if (!lp.isDecor) continue;\r
+\r
+                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;\r
+                    int childTop = 0;\r
+                    switch (vgrav) {\r
+                        default:\r
+                            childTop = paddingTop;\r
+                            break;\r
+                        case Gravity.TOP:\r
+                            childTop = paddingTop;\r
+                            paddingTop += child.getHeight();\r
+                            break;\r
+                        case Gravity.CENTER_VERTICAL:\r
+                            childTop = Math.max((height - child.getMeasuredHeight()) / 2,\r
+                                    paddingTop);\r
+                            break;\r
+                        case Gravity.BOTTOM:\r
+                            childTop = height - paddingBottom - child.getMeasuredHeight();\r
+                            paddingBottom += child.getMeasuredHeight();\r
+                            break;\r
+                    }\r
+                    childTop += scrollY;\r
+\r
+                    final int childOffset = childTop - child.getTop();\r
+                    if (childOffset != 0) {\r
+                        child.offsetTopAndBottom(childOffset);\r
+                    }\r
+                }\r
+            }\r
+\r
+            if (mOnPageChangeListener != null) {\r
+                mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);\r
+            }\r
+            if (mInternalPageChangeListener != null) {\r
+                mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);\r
+            }\r
+\r
+            if (mPageTransformer != null) {\r
+                final int scrollY = getScrollY();\r
+                final int childCount = getChildCount();\r
+                for (int i = 0; i < childCount; i++) {\r
+                    final View child = getChildAt(i);\r
+                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();\r
+\r
+                    if (lp.isDecor) continue;\r
+\r
+                    final float transformPos\r
+                            = (float) (child.getTop() - scrollY) / getClientHeight();\r
+                    mPageTransformer.transformPage(child, transformPos);\r
+                }\r
+            }\r
+        }\r
+\r
+        mCalledSuper = true;\r
+    }\r
+\r
+    private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) {\r
+        if (mOnPageChangeListener != null) {\r
+            mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);\r
+        }\r
+        if (mOnPageChangeListeners != null) {\r
+            for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {\r
+                OnPageChangeListener listener = mOnPageChangeListeners.get(i);\r
+                if (listener != null) {\r
+                    listener.onPageScrolled(position, offset, offsetPixels);\r
+                }\r
+            }\r
+        }\r
+        if (mInternalPageChangeListener != null) {\r
+            mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);\r
+        }\r
+    }\r
+\r
+    private void dispatchOnPageSelected(int position) {\r
+        if (mOnPageChangeListener != null) {\r
+            mOnPageChangeListener.onPageSelected(position);\r
+        }\r
+        if (mOnPageChangeListeners != null) {\r
+            for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {\r
+                OnPageChangeListener listener = mOnPageChangeListeners.get(i);\r
+                if (listener != null) {\r
+                    listener.onPageSelected(position);\r
+                }\r
+            }\r
+        }\r
+        if (mInternalPageChangeListener != null) {\r
+            mInternalPageChangeListener.onPageSelected(position);\r
+        }\r
+    }\r
+\r
+    private void dispatchOnScrollStateChanged(int state) {\r
+        if (mOnPageChangeListener != null) {\r
+            mOnPageChangeListener.onPageScrollStateChanged(state);\r
+        }\r
+        if (mOnPageChangeListeners != null) {\r
+            for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {\r
+                OnPageChangeListener listener = mOnPageChangeListeners.get(i);\r
+                if (listener != null) {\r
+                    listener.onPageScrollStateChanged(state);\r
+                }\r
+            }\r
+        }\r
+        if (mInternalPageChangeListener != null) {\r
+            mInternalPageChangeListener.onPageScrollStateChanged(state);\r
+        }\r
+    }\r
+\r
+    private void completeScroll(boolean postEvents) {\r
+        boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;\r
+        if (needPopulate) {\r
+            // Done with scroll, no longer want to cache view drawing.\r
+            setScrollingCacheEnabled(false);\r
+            boolean wasScrolling = !mScroller.isFinished();\r
+            if (wasScrolling) {\r
+                mScroller.abortAnimation();\r
+                int oldX = getScrollX();\r
+                int oldY = getScrollY();\r
+                int x = mScroller.getCurrX();\r
+                int y = mScroller.getCurrY();\r
+                if (oldX != x || oldY != y) {\r
+                    scrollTo(x, y);\r
+                    if (isHorizontal() && x != oldX) {\r
+                        pageScrolled(x, 0);\r
+                    }\r
+                }\r
+            }\r
+        }\r
+        mPopulatePending = false;\r
+        for (int i = 0; i < mItems.size(); i++) {\r
+            ItemInfo ii = mItems.get(i);\r
+            if (ii.scrolling) {\r
+                needPopulate = true;\r
+                ii.scrolling = false;\r
+            }\r
+        }\r
+        if (needPopulate) {\r
+            if (postEvents) {\r
+                ViewCompat.postOnAnimation(this, mEndScrollRunnable);\r
+            } else {\r
+                mEndScrollRunnable.run();\r
+            }\r
+        }\r
+    }\r
+\r
+    private boolean isGutterDrag(float x, float dx, float y, float dy) {\r
+        if (isHorizontal()) {\r
+            return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0);\r
+        } else {\r
+            return (y < mGutterSize && dy > 0) || (y > getHeight() - mGutterSize && dy < 0);\r
+        }\r
+    }\r
+\r
+    private void enableLayers(boolean enable) {\r
+        final int childCount = getChildCount();\r
+        for (int i = 0; i < childCount; i++) {\r
+            final int layerType = enable ?\r
+                    ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE;\r
+            ViewCompat.setLayerType(getChildAt(i), layerType, null);\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public boolean onInterceptTouchEvent(MotionEvent ev) {\r
+        /*\r
+         * This method JUST determines whether we want to intercept the motion.\r
+         * If we return true, onMotionEvent will be called and we do the actual\r
+         * scrolling there.\r
+         */\r
+\r
+        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;\r
+\r
+        // Always take care of the touch gesture being complete.\r
+        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {\r
+            // Release the drag.\r
+            if (DEBUG) Log.v(TAG, "Intercept done!");\r
+            if (isHorizontal()) {\r
+                resetTouch();\r
+            } else {\r
+                mIsBeingDragged = false;\r
+                mIsUnableToDrag = false;\r
+                mActivePointerId = INVALID_POINTER;\r
+                if (mVelocityTracker != null) {\r
+                    mVelocityTracker.recycle();\r
+                    mVelocityTracker = null;\r
+                }\r
+            }\r
+            return false;\r
+        }\r
+\r
+        // Nothing more to do here if we have decided whether or not we\r
+        // are dragging.\r
+        if (action != MotionEvent.ACTION_DOWN) {\r
+            if (mIsBeingDragged) {\r
+                if (DEBUG) Log.v(TAG, "Intercept returning true!");\r
+                return true;\r
+            }\r
+            if (mIsUnableToDrag) {\r
+                if (DEBUG) Log.v(TAG, "Intercept returning false!");\r
+                return false;\r
+            }\r
+        }\r
+\r
+        if (isHorizontal()) {\r
+\r
+            switch (action) {\r
+                case MotionEvent.ACTION_MOVE: {\r
+                /*\r
+                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check\r
+                 * whether the user has moved far enough from his original down touch.\r
+                 */\r
+\r
+                /*\r
+                * Locally do absolute value. mLastMotionY is set to the y value\r
+                * of the down event.\r
+                */\r
+                    final int activePointerId = mActivePointerId;\r
+                    if (activePointerId == INVALID_POINTER) {\r
+                        break;\r
+                    }\r
+\r
+                    final int pointerIndex\r
+                            = MotionEventCompat.findPointerIndex(ev, activePointerId);\r
+                    final float x = MotionEventCompat.getX(ev, pointerIndex);\r
+                    final float dx = x - mLastMotionX;\r
+                    final float xDiff = Math.abs(dx);\r
+                    final float y = MotionEventCompat.getY(ev, pointerIndex);\r
+                    final float yDiff = Math.abs(y - mInitialMotionY);\r
+\r
+                    if (dx != 0 && !isGutterDrag(mLastMotionX, dx, 0, 0) &&\r
+                            canScroll(this, false, (int) dx, 0, (int) x, (int) y)) {\r
+                        // Nested view has scrollable\r
+                        // area under this point. Let it be handled there.\r
+                        mLastMotionX = x;\r
+                        mLastMotionY = y;\r
+                        mIsUnableToDrag = true;\r
+                        return false;\r
+                    }\r
+                    if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {\r
+                        if (DEBUG) Log.v(TAG, getContext().getString(R.string.debug_start_drag));\r
+                        mIsBeingDragged = true;\r
+                        requestParentDisallowInterceptTouchEvent(true);\r
+                        setScrollState(SCROLL_STATE_DRAGGING);\r
+                        mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :\r
+                                mInitialMotionX - mTouchSlop;\r
+                        mLastMotionY = y;\r
+                        setScrollingCacheEnabled(true);\r
+                    } else if (yDiff > mTouchSlop) {\r
+                        // The finger has moved enough in the vertical\r
+                        // direction to be counted as a drag...  abort\r
+                        // any attempt to drag horizontally, to work correctly\r
+                        // with children that have scrolling containers.\r
+                        if (DEBUG)\r
+                            Log.v(TAG, getContext().getString(R.string.debug_start_unable_drag));\r
+                        mIsUnableToDrag = true;\r
+                    }\r
+                    if (mIsBeingDragged && performDrag(x, 0)) {\r
+                        // Scroll to follow the motion event\r
+                        ViewCompat.postInvalidateOnAnimation(this);\r
+                    }\r
+                    break;\r
+                }\r
+\r
+                case MotionEvent.ACTION_DOWN: {\r
+                /*\r
+                 * Remember location of down touch.\r
+                 * ACTION_DOWN always refers to pointer index 0.\r
+                 */\r
+                    mLastMotionX = mInitialMotionX = ev.getX();\r
+                    mLastMotionY = mInitialMotionY = ev.getY();\r
+                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);\r
+                    mIsUnableToDrag = false;\r
+\r
+                    mIsScrollStarted = true;\r
+                    mScroller.computeScrollOffset();\r
+                    if (mScrollState == SCROLL_STATE_SETTLING &&\r
+                            Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {\r
+                        // Let the user 'catch' the pager as it animates.\r
+                        mScroller.abortAnimation();\r
+                        mPopulatePending = false;\r
+                        populate();\r
+                        mIsBeingDragged = true;\r
+                        requestParentDisallowInterceptTouchEvent(true);\r
+                        setScrollState(SCROLL_STATE_DRAGGING);\r
+                    } else {\r
+                        completeScroll(false);\r
+                        mIsBeingDragged = false;\r
+                    }\r
+\r
+                    if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY\r
+                            + " mIsBeingDragged=" + mIsBeingDragged\r
+                            + "mIsUnableToDrag=" + mIsUnableToDrag);\r
+                    break;\r
+                }\r
+\r
+                case MotionEventCompat.ACTION_POINTER_UP:\r
+                    onSecondaryPointerUp(ev);\r
+                    break;\r
+            }\r
+\r
+           /* if (mVelocityTracker == null) {\r
+                mVelocityTracker = VelocityTracker.obtain();\r
+            }\r
+            mVelocityTracker.addMovement(ev);\r
+*/\r
+        /*\r
+         * The only time we want to intercept motion events is if we are in the\r
+         * drag mode.\r
+         */\r
+        } else {\r
+            switch (action) {\r
+                case MotionEvent.ACTION_MOVE: {\r
+                /*\r
+                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check\r
+                 * whether the user has moved far enough from his original down touch.\r
+                 */\r
+\r
+                /*\r
+                * Locally do absolute value. mLastMotionY is set to the y value\r
+                * of the down event.\r
+                */\r
+                    final int activePointerId = mActivePointerId;\r
+                    if (activePointerId == INVALID_POINTER) {\r
+                        // If we don't have a valid id, the touch down wasn't on content.\r
+                        break;\r
+                    }\r
+\r
+                    final int pointerIndex\r
+                            = MotionEventCompat.findPointerIndex(ev, activePointerId);\r
+                    final float y = MotionEventCompat.getY(ev, pointerIndex);\r
+                    final float dy = y - mLastMotionY;\r
+                    final float yDiff = Math.abs(dy);\r
+                    final float x\r
+                                = MotionEventCompat.getX(ev, pointerIndex);\r
+                    final float xDiff = Math.abs(x - mInitialMotionX);\r
+\r
+                    if (dy != 0 && !isGutterDrag(0, 0, mLastMotionY, dy) &&\r
+                            canScroll(this, false, 0,\r
+                                    (int) dy, (int) x, (int) y)) {\r
+                        // Nested view has scrollable\r
+                        // area under this point.\r
+                        // Let it be handled there.\r
+                        mLastMotionX = x;\r
+                        mLastMotionY = y;\r
+                        mIsUnableToDrag = true;\r
+                        return false;\r
+                    }\r
+                    if (yDiff > mTouchSlop && yDiff * 0.5f > xDiff) {\r
+                        if (DEBUG) Log.v(TAG, getContext().getString(R.string.debug_start_drag));\r
+                        mIsBeingDragged = true;\r
+                        requestParentDisallowInterceptTouchEvent(true);\r
+                        setScrollState(SCROLL_STATE_DRAGGING);\r
+                        mLastMotionY = dy > 0 ? mInitialMotionY + mTouchSlop :\r
+                                mInitialMotionY - mTouchSlop;\r
+                        mLastMotionX = x;\r
+                        setScrollingCacheEnabled(true);\r
+                    } else if (xDiff > mTouchSlop) {\r
+                        // The finger has moved enough in the vertical\r
+                        // direction to be counted as a drag...  abort\r
+                        // any attempt to drag horizontally, to work correctly\r
+                        // with children that have scrolling containers.\r
+                        if (DEBUG)\r
+                            Log.v(TAG, getContext().getString(R.string.debug_start_unable_drag));\r
+                        mIsUnableToDrag = true;\r
+                    }\r
+                    if (mIsBeingDragged && performDrag(0, y)) {\r
+                        // Scroll to follow the motion event\r
+                        ViewCompat.postInvalidateOnAnimation(this);\r
+                    }\r
+                    break;\r
+                }\r
+\r
+                case MotionEvent.ACTION_DOWN: {\r
+                /*\r
+                 * Remember location of down touch.\r
+                 * ACTION_DOWN always refers to pointer index 0.\r
+                 */\r
+                    mLastMotionX = mInitialMotionX = ev.getX();\r
+                    mLastMotionY = mInitialMotionY = ev.getY();\r
+                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);\r
+                    mIsUnableToDrag = false;\r
+\r
+                    mScroller.computeScrollOffset();\r
+                    if (mScrollState == SCROLL_STATE_SETTLING &&\r
+                            Math.abs(mScroller.getFinalY() - mScroller.getCurrY()) > mCloseEnough) {\r
+                        // Let the user 'catch' the pager as it animates.\r
+                        mScroller.abortAnimation();\r
+                        mPopulatePending = false;\r
+                        populate();\r
+                        mIsBeingDragged = true;\r
+                        requestParentDisallowInterceptTouchEvent(true);\r
+                        setScrollState(SCROLL_STATE_DRAGGING);\r
+                    } else {\r
+                        completeScroll(false);\r
+                        mIsBeingDragged = false;\r
+                    }\r
+\r
+                    if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY\r
+                            + " mIsBeingDragged=" + mIsBeingDragged\r
+                            + "mIsUnableToDrag=" + mIsUnableToDrag);\r
+                    break;\r
+                }\r
+\r
+                case MotionEventCompat.ACTION_POINTER_UP:\r
+                    onSecondaryPointerUp(ev);\r
+                    break;\r
+            }\r
+\r
+        }\r
+        if (mVelocityTracker == null) {\r
+            mVelocityTracker = VelocityTracker.obtain();\r
+        }\r
+        mVelocityTracker.addMovement(ev);\r
+        return mIsBeingDragged;\r
+    }\r
+\r
+    @Override\r
+    public boolean onTouchEvent(MotionEvent ev) {\r
+        if (mFakeDragging) {\r
+            // A fake drag is in progress already, ignore this real one\r
+            // but still eat the touch events.\r
+            // (It is likely that the user is multi-touching the screen.)\r
+            return true;\r
+        }\r
+\r
+        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {\r
+            // Don't handle edge touches immediately -- they may actually belong to one of our\r
+            // descendants.\r
+            return false;\r
+        }\r
+\r
+        if (mAdapter == null || mAdapter.getCount() == 0) {\r
+            // Nothing to present or scroll; nothing to touch.\r
+            return false;\r
+        }\r
+\r
+        if (mVelocityTracker == null) {\r
+            mVelocityTracker = VelocityTracker.obtain();\r
+        }\r
+        mVelocityTracker.addMovement(ev);\r
+\r
+        final int action = ev.getAction();\r
+        boolean needsInvalidate = false;\r
+\r
+        if (isHorizontal()) {\r
+            switch (action & MotionEventCompat.ACTION_MASK) {\r
+                case MotionEvent.ACTION_DOWN: {\r
+                    mScroller.abortAnimation();\r
+                    mPopulatePending = false;\r
+                    populate();\r
+\r
+                    // Remember where the motion event started\r
+                    mLastMotionX = mInitialMotionX = ev.getX();\r
+                    mLastMotionY = mInitialMotionY = ev.getY();\r
+                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);\r
+                    break;\r
+                }\r
+                case MotionEvent.ACTION_MOVE:\r
+                    if (!mIsBeingDragged) {\r
+                        final int pointerIndex\r
+                                = MotionEventCompat.findPointerIndex(ev,\r
+                                    mActivePointerId);\r
+                        if (pointerIndex == -1) {\r
+                            // A child has consumed some\r
+                            // touch events and put us into an inconsistent state.\r
+                            needsInvalidate = resetTouch();\r
+                            break;\r
+                        }\r
+                        final float x = MotionEventCompat.getX(ev, pointerIndex);\r
+                        final float xDiff = Math.abs(x - mLastMotionX);\r
+                        final float y\r
+                                = MotionEventCompat.getY(ev,\r
+                                    pointerIndex);\r
+                        final float yDiff = Math.abs(y - mLastMotionY);\r
+                        if (xDiff > mTouchSlop && xDiff > yDiff) {\r
+                            if (DEBUG)\r
+                                Log.v(TAG, getContext().getString(R.string.debug_start_drag));\r
+                            mIsBeingDragged = true;\r
+                            requestParentDisallowInterceptTouchEvent(true);\r
+                            mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :\r
+                                    mInitialMotionX - mTouchSlop;\r
+                            mLastMotionY = y;\r
+                            setScrollState(SCROLL_STATE_DRAGGING);\r
+                            setScrollingCacheEnabled(true);\r
+\r
+                            // Disallow Parent Intercept, just in case\r
+                            ViewParent parent = getParent();\r
+                            if (parent != null) {\r
+                                parent.requestDisallowInterceptTouchEvent(true);\r
+                            }\r
+                        }\r
+                    }\r
+                    // Not else! Note that mIsBeingDragged can be set above.\r
+                    if (mIsBeingDragged) {\r
+                        // Scroll to follow the motion event\r
+                        final int activePointerIndex = MotionEventCompat.findPointerIndex(\r
+                                ev, mActivePointerId);\r
+                        final float x = MotionEventCompat.getX(ev, activePointerIndex);\r
+                        needsInvalidate |= performDrag(x, 0);\r
+                    }\r
+                    break;\r
+                case MotionEvent.ACTION_UP:\r
+                    if (mIsBeingDragged) {\r
+                        final VelocityTracker velocityTracker = mVelocityTracker;\r
+                        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);\r
+                        int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(\r
+                                velocityTracker, mActivePointerId);\r
+                        mPopulatePending = true;\r
+                        final int width = getClientWidth();\r
+                        final int scrollX = getScrollX();\r
+                        final ItemInfo ii = infoForCurrentScrollPosition();\r
+                        final float marginOffset = (float) mPageMargin / width;\r
+                        final int currentPage = ii.position;\r
+                        final float pageOffset = (((float) scrollX / width) - ii.offset)\r
+                                / (ii.widthFactor + marginOffset);\r
+                        final int activePointerIndex =\r
+                                MotionEventCompat.findPointerIndex(ev, mActivePointerId);\r
+                        final float x = MotionEventCompat.getX(ev, activePointerIndex);\r
+                        final int totalDelta = (int) (x - mInitialMotionX);\r
+                        int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,\r
+                                totalDelta, 0);\r
+                        setCurrentItemInternal(nextPage, true, true, initialVelocity);\r
+\r
+                        needsInvalidate = resetTouch();\r
+                    }\r
+                    break;\r
+                case MotionEvent.ACTION_CANCEL:\r
+                    if (mIsBeingDragged) {\r
+                        scrollToItem(mCurItem, true, 0, false);\r
+                        needsInvalidate = resetTouch();\r
+                    }\r
+                    break;\r
+                case MotionEventCompat.ACTION_POINTER_DOWN: {\r
+                    final int index = MotionEventCompat.getActionIndex(ev);\r
+                    final float x = MotionEventCompat.getX(ev, index);\r
+                    mLastMotionX = x;\r
+                    mActivePointerId = MotionEventCompat.getPointerId(ev, index);\r
+                    break;\r
+                }\r
+                case MotionEventCompat.ACTION_POINTER_UP:\r
+                    onSecondaryPointerUp(ev);\r
+                    mLastMotionX = MotionEventCompat.getX(ev,\r
+                            MotionEventCompat.findPointerIndex(ev, mActivePointerId));\r
+                    break;\r
+            }\r
+        } else {\r
+            switch (action & MotionEventCompat.ACTION_MASK) {\r
+                case MotionEvent.ACTION_DOWN: {\r
+                    mScroller.abortAnimation();\r
+                    mPopulatePending = false;\r
+                    populate();\r
+\r
+                    // Remember where the motion event started\r
+                    mLastMotionX = mInitialMotionX = ev.getX();\r
+                    mLastMotionY = mInitialMotionY = ev.getY();\r
+                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);\r
+                    break;\r
+                }\r
+                case MotionEvent.ACTION_MOVE:\r
+                    if (!mIsBeingDragged) {\r
+                        final int pointerIndex =\r
+                                MotionEventCompat.findPointerIndex(ev, mActivePointerId);\r
+                        final float y = MotionEventCompat.getY(ev, pointerIndex);\r
+                        final float yDiff\r
+                                = Math.abs(y - mLastMotionY);\r
+                        final float x\r
+                                = MotionEventCompat.getX(ev, pointerIndex);\r
+                        final float xDiff = Math.abs(x - mLastMotionX);\r
+\r
+                        if (yDiff > mTouchSlop && yDiff > xDiff) {\r
+                            if (DEBUG)\r
+                                Log.v(TAG, getContext().getString(R.string.debug_start_drag));\r
+                            mIsBeingDragged = true;\r
+                            requestParentDisallowInterceptTouchEvent(true);\r
+                            mLastMotionY = y - mInitialMotionY > 0 ? mInitialMotionY + mTouchSlop :\r
+                                    mInitialMotionY - mTouchSlop;\r
+                            mLastMotionX = x;\r
+                            setScrollState(SCROLL_STATE_DRAGGING);\r
+                            setScrollingCacheEnabled(true);\r
+\r
+                            // Disallow Parent Intercept, just in case\r
+                            ViewParent parent = getParent();\r
+                            if (parent != null) {\r
+                                parent.requestDisallowInterceptTouchEvent(true);\r
+                            }\r
+                        }\r
+                    }\r
+                    // Not else! Note that mIsBeingDragged can be set above.\r
+                    if (mIsBeingDragged) {\r
+                        // Scroll to follow the motion event\r
+                        final int activePointerIndex = MotionEventCompat.findPointerIndex(\r
+                                ev, mActivePointerId);\r
+                        final float y = MotionEventCompat.getY(ev, activePointerIndex);\r
+                        needsInvalidate |= performDrag(0, y);\r
+                    }\r
+                    break;\r
+                case MotionEvent.ACTION_UP:\r
+                    if (mIsBeingDragged) {\r
+                        final VelocityTracker velocityTracker = mVelocityTracker;\r
+                        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);\r
+                        int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(\r
+                                velocityTracker, mActivePointerId);\r
+                        mPopulatePending = true;\r
+                        final int height = getClientHeight();\r
+                        final int scrollY = getScrollY();\r
+                        final ItemInfo ii = infoForCurrentScrollPosition();\r
+                        final int currentPage = ii.position;\r
+                        final float pageOffset =\r
+                                (((float) scrollY / height) - ii.offset) / ii.heightFactor;\r
+                        final int activePointerIndex =\r
+                                MotionEventCompat.findPointerIndex(ev, mActivePointerId);\r
+                        final float y = MotionEventCompat.getY(ev, activePointerIndex);\r
+                        final int totalDelta = (int) (y - mInitialMotionY);\r
+                        int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,\r
+                                0, totalDelta);\r
+                        setCurrentItemInternal(nextPage, true, true, initialVelocity);\r
+\r
+                        mActivePointerId = INVALID_POINTER;\r
+                        endDrag();\r
+                        needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();\r
+                    }\r
+                    break;\r
+                case MotionEvent.ACTION_CANCEL:\r
+                    if (mIsBeingDragged) {\r
+                        scrollToItem(mCurItem, true, 0, false);\r
+                        mActivePointerId = INVALID_POINTER;\r
+                        endDrag();\r
+                        needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();\r
+                    }\r
+                    break;\r
+                case MotionEventCompat.ACTION_POINTER_DOWN: {\r
+                    final int index = MotionEventCompat.getActionIndex(ev);\r
+                    final float y = MotionEventCompat.getY(ev, index);\r
+                    mLastMotionY = y;\r
+                    mActivePointerId = MotionEventCompat.getPointerId(ev, index);\r
+                    break;\r
+                }\r
+                case MotionEventCompat.ACTION_POINTER_UP:\r
+                    onSecondaryPointerUp(ev);\r
+                    mLastMotionY = MotionEventCompat.getY(ev,\r
+                            MotionEventCompat.findPointerIndex(ev, mActivePointerId));\r
+                    break;\r
+            }\r
+        }\r
+        if (needsInvalidate) {\r
+            ViewCompat.postInvalidateOnAnimation(this);\r
+        }\r
+        return true;\r
+    }\r
+\r
+    private boolean resetTouch() {\r
+        boolean needsInvalidate;\r
+        mActivePointerId = INVALID_POINTER;\r
+        endDrag();\r
+        needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();\r
+        return needsInvalidate;\r
+    }\r
+\r
+    private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {\r
+        final ViewParent parent = getParent();\r
+        if (parent != null) {\r
+            parent.requestDisallowInterceptTouchEvent(disallowIntercept);\r
+        }\r
+    }\r
+\r
+    private boolean performDrag(float x, float y) {\r
+        boolean needsInvalidate = false;\r
+        if (isHorizontal()) {\r
+            final float deltaX = mLastMotionX - x;\r
+            mLastMotionX = x;\r
+\r
+            float oldScrollX = getScrollX();\r
+            float scrollX = oldScrollX + deltaX;\r
+            final int width = getClientWidth();\r
+\r
+            float leftBound = width * mFirstOffset;\r
+            float rightBound = width * mLastOffset;\r
+            boolean leftAbsolute = true;\r
+            boolean rightAbsolute = true;\r
+\r
+            final ItemInfo firstItem = mItems.get(0);\r
+            final ItemInfo lastItem = mItems.get(mItems.size() - 1);\r
+            if (firstItem.position != 0) {\r
+                leftAbsolute = false;\r
+                leftBound = firstItem.offset * width;\r
+            }\r
+            if (lastItem.position != mAdapter.getCount() - 1) {\r
+                rightAbsolute = false;\r
+                rightBound = lastItem.offset * width;\r
+            }\r
+\r
+            if (scrollX < leftBound) {\r
+                if (leftAbsolute) {\r
+                    float over = leftBound - scrollX;\r
+                    needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);\r
+                }\r
+                scrollX = leftBound;\r
+            } else if (scrollX > rightBound) {\r
+                if (rightAbsolute) {\r
+                    float over = scrollX - rightBound;\r
+                    needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);\r
+                }\r
+                scrollX = rightBound;\r
+            }\r
+            // Don't lose the rounded component\r
+            mLastMotionX += scrollX - (int) scrollX;\r
+            scrollTo((int) scrollX, getScrollY());\r
+            pageScrolled((int) scrollX, 0);\r
+        } else {\r
+\r
+            final float deltaY = mLastMotionY - y;\r
+            mLastMotionY = y;\r
+\r
+            float oldScrollY = getScrollY();\r
+            float scrollY = oldScrollY + deltaY;\r
+            final int height = getClientHeight();\r
+\r
+            float topBound = height * mFirstOffset;\r
+            float bottomBound = height * mLastOffset;\r
+            boolean topAbsolute = true;\r
+            boolean bottomAbsolute = true;\r
+\r
+            final ItemInfo firstItem = mItems.get(0);\r
+            final ItemInfo lastItem = mItems.get(mItems.size() - 1);\r
+            if (firstItem.position != 0) {\r
+                topAbsolute = false;\r
+                topBound = firstItem.offset * height;\r
+            }\r
+            if (lastItem.position != mAdapter.getCount() - 1) {\r
+                bottomAbsolute = false;\r
+                bottomBound = lastItem.offset * height;\r
+            }\r
+\r
+            if (scrollY < topBound) {\r
+                if (topAbsolute) {\r
+                    float over = topBound - scrollY;\r
+                    needsInvalidate = mTopEdge.onPull(Math.abs(over) / height);\r
+                }\r
+                scrollY = topBound;\r
+            } else if (scrollY > bottomBound) {\r
+                if (bottomAbsolute) {\r
+                    float over = scrollY - bottomBound;\r
+                    needsInvalidate = mBottomEdge.onPull(Math.abs(over) / height);\r
+                }\r
+                scrollY = bottomBound;\r
+            }\r
+            // Don't lose the rounded component\r
+            mLastMotionX += scrollY - (int) scrollY;\r
+            scrollTo(getScrollX(), (int) scrollY);\r
+            pageScrolled(0, (int) scrollY);\r
+        }\r
+\r
+        return needsInvalidate;\r
+    }\r
+\r
+    /**\r
+     * @return Info about the page at the current scroll position.\r
+     * This can be synthetic for a missing middle page; the 'object' field can be null.\r
+     */\r
+    private ItemInfo infoForCurrentScrollPosition() {\r
+        ItemInfo lastItem = null;\r
+        if (isHorizontal()) {\r
+            final int width = getClientWidth();\r
+            final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0;\r
+            final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;\r
+            int lastPos = -1;\r
+            float lastOffset = 0.f;\r
+            float lastWidth = 0.f;\r
+            boolean first = true;\r
+\r
+            for (int i = 0; i < mItems.size(); i++) {\r
+                ItemInfo ii = mItems.get(i);\r
+                float offset;\r
+                if (!first && ii.position != lastPos + 1) {\r
+                    // Create a synthetic item for a missing page.\r
+                    ii = mTempItem;\r
+                    ii.offset = lastOffset + lastWidth + marginOffset;\r
+                    ii.position = lastPos + 1;\r
+                    ii.widthFactor = mAdapter.getPageWidth(ii.position);\r
+                    i--;\r
+                }\r
+                offset = ii.offset;\r
+\r
+                final float leftBound = offset;\r
+                final float rightBound = offset + ii.widthFactor + marginOffset;\r
+                if (first || scrollOffset >= leftBound) {\r
+                    if (scrollOffset < rightBound || i == mItems.size() - 1) {\r
+                        return ii;\r
+                    }\r
+                } else {\r
+                    return lastItem;\r
+                }\r
+                first = false;\r
+                lastPos = ii.position;\r
+                lastOffset = offset;\r
+                lastWidth = ii.widthFactor;\r
+                lastItem = ii;\r
+            }\r
+        } else {\r
+            final int height = getClientHeight();\r
+            final float scrollOffset = height > 0 ? (float) getScrollY() / height : 0;\r
+            final float marginOffset = height > 0 ? (float) mPageMargin / height : 0;\r
+            int lastPos = -1;\r
+            float lastOffset = 0.f;\r
+            float lastHeight = 0.f;\r
+            boolean first = true;\r
+\r
+            for (int i = 0; i < mItems.size(); i++) {\r
+                ItemInfo ii = mItems.get(i);\r
+                float offset;\r
+                if (!first && ii.position != lastPos + 1) {\r
+                    // Create a synthetic item for a missing page.\r
+                    ii = mTempItem;\r
+                    ii.offset = lastOffset + lastHeight + marginOffset;\r
+                    ii.position = lastPos + 1;\r
+                    ii.heightFactor = mAdapter.getPageWidth(ii.position);\r
+                    i--;\r
+                }\r
+                offset = ii.offset;\r
+\r
+                final float topBound = offset;\r
+                final float bottomBound = offset + ii.heightFactor + marginOffset;\r
+                if (first || scrollOffset >= topBound) {\r
+                    if (scrollOffset < bottomBound || i == mItems.size() - 1) {\r
+                        return ii;\r
+                    }\r
+                } else {\r
+                    return lastItem;\r
+                }\r
+                first = false;\r
+                lastPos = ii.position;\r
+                lastOffset = offset;\r
+                lastHeight = ii.heightFactor;\r
+                lastItem = ii;\r
+            }\r
+        }\r
+\r
+        return lastItem;\r
+    }\r
+\r
+    private int determineTargetPage(int currentPage, float pageOffset,\r
+                                        int velocity, int deltaX, int deltaY) {\r
+        int targetPage;\r
+        if (isHorizontal()) {\r
+            if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {\r
+                targetPage = velocity > 0 ? currentPage : currentPage + 1;\r
+            } else {\r
+                final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;\r
+                targetPage = (int) (currentPage + pageOffset + truncator);\r
+            }\r
+        } else {\r
+            if (Math.abs(deltaY) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {\r
+                targetPage = velocity > 0 ? currentPage : currentPage + 1;\r
+            } else {\r
+                final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;\r
+                targetPage = (int) (currentPage + pageOffset + truncator);\r
+            }\r
+        }\r
+\r
+        if (mItems.size() > 0) {\r
+            final ItemInfo firstItem = mItems.get(0);\r
+            final ItemInfo lastItem = mItems.get(mItems.size() - 1);\r
+\r
+            // Only let the user target pages we have items for\r
+            targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));\r
+        }\r
+\r
+        return targetPage;\r
+    }\r
+\r
+    @Override\r
+    public void draw(Canvas canvas) {\r
+        super.draw(canvas);\r
+        boolean needsInvalidate = false;\r
+\r
+        final int overScrollMode = ViewCompat.getOverScrollMode(this);\r
+        if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||\r
+                (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&\r
+                        mAdapter != null && mAdapter.getCount() > 1)) {\r
+            if (isHorizontal()) {\r
+                if (!mLeftEdge.isFinished()) {\r
+                    final int restoreCount = canvas.save();\r
+                    final int height = getHeight() - getPaddingTop() - getPaddingBottom();\r
+                    final int width = getWidth();\r
+\r
+                    canvas.rotate(270);\r
+                    canvas.translate(-height + getPaddingTop(), mFirstOffset * width);\r
+                    mLeftEdge.setSize(height, width);\r
+                    needsInvalidate |= mLeftEdge.draw(canvas);\r
+                    canvas.restoreToCount(restoreCount);\r
+                }\r
+                if (!mRightEdge.isFinished()) {\r
+                    final int restoreCount = canvas.save();\r
+                    final int width = getWidth();\r
+                    final int height = getHeight() - getPaddingTop() - getPaddingBottom();\r
+\r
+                    canvas.rotate(90);\r
+                    canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width);\r
+                    mRightEdge.setSize(height, width);\r
+                    needsInvalidate |= mRightEdge.draw(canvas);\r
+                    canvas.restoreToCount(restoreCount);\r
+                } else {\r
+                    mLeftEdge.finish();\r
+                    mRightEdge.finish();\r
+                }\r
+            } else {\r
+                if (!mTopEdge.isFinished()) {\r
+                    final int restoreCount = canvas.save();\r
+                    final int height = getHeight();\r
+                    final int width = getWidth() - getPaddingLeft() - getPaddingRight();\r
+\r
+                    canvas.translate(getPaddingLeft(), mFirstOffset * height);\r
+                    mTopEdge.setSize(width, height);\r
+                    needsInvalidate |= mTopEdge.draw(canvas);\r
+                    canvas.restoreToCount(restoreCount);\r
+                }\r
+                if (!mBottomEdge.isFinished()) {\r
+                    final int restoreCount = canvas.save();\r
+                    final int height = getHeight();\r
+                    final int width = getWidth() - getPaddingLeft() - getPaddingRight();\r
+\r
+                    canvas.rotate(180);\r
+                    canvas.translate(-width - getPaddingLeft(), -(mLastOffset + 1) * height);\r
+                    mBottomEdge.setSize(width, height);\r
+                    needsInvalidate |= mBottomEdge.draw(canvas);\r
+                    canvas.restoreToCount(restoreCount);\r
+                } else {\r
+                    mTopEdge.finish();\r
+                    mBottomEdge.finish();\r
+                }\r
+            }\r
+        }\r
+\r
+\r
+        if (needsInvalidate) {\r
+            // Keep animating\r
+            ViewCompat.postInvalidateOnAnimation(this);\r
+        }\r
+    }\r
+\r
+    @Override\r
+    protected void onDraw(Canvas canvas) {\r
+        super.onDraw(canvas);\r
+\r
+        // Draw the margin drawable between pages if needed.\r
+        if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {\r
+            if (isHorizontal()) {\r
+                final int scrollX = getScrollX();\r
+                final int width = getWidth();\r
+\r
+                final float marginOffset = (float) mPageMargin / width;\r
+                int itemIndex = 0;\r
+                ItemInfo ii = mItems.get(0);\r
+                float offset = ii.offset;\r
+                final int itemCount = mItems.size();\r
+                final int firstPos = ii.position;\r
+                final int lastPos = mItems.get(itemCount - 1).position;\r
+                for (int pos = firstPos; pos < lastPos; pos++) {\r
+                    while (pos > ii.position && itemIndex < itemCount) {\r
+                        ii = mItems.get(++itemIndex);\r
+                    }\r
+\r
+                    float drawAt;\r
+                    if (pos == ii.position) {\r
+                        drawAt = (ii.offset + ii.widthFactor) * width;\r
+                        offset = ii.offset + ii.widthFactor + marginOffset;\r
+                    } else {\r
+                        float widthFactor = mAdapter.getPageWidth(pos);\r
+                        drawAt = (offset + widthFactor) * width;\r
+                        offset += widthFactor + marginOffset;\r
+                    }\r
+\r
+                    if (drawAt + mPageMargin > scrollX) {\r
+                        mMarginDrawable.setBounds(Math.round(drawAt), mTopPageBounds,\r
+                                Math.round(drawAt + mPageMargin), mBottomPageBounds);\r
+                        mMarginDrawable.draw(canvas);\r
+                    }\r
+\r
+                    if (drawAt > scrollX + width) {\r
+                        break; // No more visible, no sense in continuing\r
+                    }\r
+                }\r
+            } else {\r
+                final int scrollY = getScrollY();\r
+                final int height = getHeight();\r
+\r
+                final float marginOffset = (float) mPageMargin / height;\r
+                int itemIndex = 0;\r
+                ItemInfo ii = mItems.get(0);\r
+                float offset = ii.offset;\r
+                final int itemCount = mItems.size();\r
+                final int firstPos = ii.position;\r
+                final int lastPos = mItems.get(itemCount - 1).position;\r
+                for (int pos = firstPos; pos < lastPos; pos++) {\r
+                    while (pos > ii.position && itemIndex < itemCount) {\r
+                        ii = mItems.get(++itemIndex);\r
+                    }\r
+\r
+                    float drawAt;\r
+                    if (pos == ii.position) {\r
+                        drawAt = (ii.offset + ii.heightFactor) * height;\r
+                        offset = ii.offset + ii.heightFactor + marginOffset;\r
+                    } else {\r
+                        float heightFactor = mAdapter.getPageWidth(pos);\r
+                        drawAt = (offset + heightFactor) * height;\r
+                        offset += heightFactor + marginOffset;\r
+                    }\r
+\r
+                    if (drawAt + mPageMargin > scrollY) {\r
+                        mMarginDrawable.setBounds(mLeftPageBounds, (int) drawAt,\r
+                                mRightPageBounds, (int) (drawAt + mPageMargin + 0.5f));\r
+                        mMarginDrawable.draw(canvas);\r
+                    }\r
+\r
+                    if (drawAt > scrollY + height) {\r
+                        break; // No more visible, no sense in continuing\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Start a fake drag of the pager.\r
+     * <p>\r
+     * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager\r
+     * with the touch scrolling of another view, while still letting the ViewPager\r
+     * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.)\r
+     * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call\r
+     * {@link #endFakeDrag()} to complete the fake drag and fling as necessary.\r
+     * <p>\r
+     * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag\r
+     * is already in progress, this method will return false.\r
+     *\r
+     * @return true if the fake drag began successfully, false if it could not be started.\r
+     * @see #fakeDragBy(float)\r
+     * @see #endFakeDrag()\r
+     */\r
+    public boolean beginFakeDrag() {\r
+        if (mIsBeingDragged) {\r
+            return false;\r
+        }\r
+        mFakeDragging = true;\r
+        setScrollState(SCROLL_STATE_DRAGGING);\r
+        if (isHorizontal()) {\r
+            mInitialMotionX = mLastMotionX = 0;\r
+        } else {\r
+            mInitialMotionY = mLastMotionY = 0;\r
+        }\r
+        if (mVelocityTracker == null) {\r
+            mVelocityTracker = VelocityTracker.obtain();\r
+        } else {\r
+            mVelocityTracker.clear();\r
+        }\r
+        final long time = SystemClock.uptimeMillis();\r
+        final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);\r
+        mVelocityTracker.addMovement(ev);\r
+        ev.recycle();\r
+        mFakeDragBeginTime = time;\r
+        return true;\r
+    }\r
+\r
+    /**\r
+     * End a fake drag of the pager.\r
+     *\r
+     * @see #beginFakeDrag()\r
+     * @see #endFakeDrag()\r
+     */\r
+    public void endFakeDrag() {\r
+        if (!mFakeDragging) {\r
+            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");\r
+        }\r
+\r
+        if (mAdapter != null) {\r
+            if (isHorizontal()) {\r
+                final VelocityTracker velocityTracker = mVelocityTracker;\r
+                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);\r
+                int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(\r
+                        velocityTracker, mActivePointerId);\r
+                mPopulatePending = true;\r
+                final int width = getClientWidth();\r
+                final int scrollX = getScrollX();\r
+                final ItemInfo ii = infoForCurrentScrollPosition();\r
+                final int currentPage = ii.position;\r
+                final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;\r
+                final int totalDelta = (int) (mLastMotionX - mInitialMotionX);\r
+                int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,\r
+                        totalDelta, 0);\r
+                setCurrentItemInternal(nextPage, true, true, initialVelocity);\r
+            } else {\r
+                final VelocityTracker velocityTracker = mVelocityTracker;\r
+                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);\r
+                int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(\r
+                        velocityTracker, mActivePointerId);\r
+                mPopulatePending = true;\r
+                final int height = getClientHeight();\r
+                final int scrollY = getScrollY();\r
+                final ItemInfo ii = infoForCurrentScrollPosition();\r
+                final int currentPage = ii.position;\r
+                final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor;\r
+                final int totalDelta = (int) (mLastMotionY - mInitialMotionY);\r
+                int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,\r
+                        0, totalDelta);\r
+                setCurrentItemInternal(nextPage, true, true, initialVelocity);\r
+            }\r
+        }\r
+        endDrag();\r
+\r
+        mFakeDragging = false;\r
+    }\r
+\r
+    /**\r
+     * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.\r
+     *\r
+     * @param xOffset Offset in pixels to drag by.\r
+     * @see #beginFakeDrag()\r
+     * @see #endFakeDrag()\r
+     */\r
+    public void fakeDragBy(float xOffset, float yOffset) {\r
+        MotionEvent ev = null;\r
+        if (!mFakeDragging) {\r
+            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");\r
+        }\r
+\r
+        if (mAdapter == null) {\r
+            return;\r
+        }\r
+\r
+        if (isHorizontal()) {\r
+            mLastMotionX += xOffset;\r
+\r
+            float oldScrollX = getScrollX();\r
+            float scrollX = oldScrollX - xOffset;\r
+            final int width = getClientWidth();\r
+\r
+            float leftBound = width * mFirstOffset;\r
+            float rightBound = width * mLastOffset;\r
+\r
+            final ItemInfo firstItem = mItems.get(0);\r
+            final ItemInfo lastItem = mItems.get(mItems.size() - 1);\r
+            if (firstItem.position != 0) {\r
+                leftBound = firstItem.offset * width;\r
+            }\r
+            if (lastItem.position != mAdapter.getCount() - 1) {\r
+                rightBound = lastItem.offset * width;\r
+            }\r
+\r
+            if (scrollX < leftBound) {\r
+                scrollX = leftBound;\r
+            } else if (scrollX > rightBound) {\r
+                scrollX = rightBound;\r
+            }\r
+            // Don't lose the rounded component\r
+            mLastMotionX += scrollX - (int) scrollX;\r
+            scrollTo((int) scrollX, getScrollY());\r
+            pageScrolled((int) scrollX, 0);\r
+\r
+            // Synthesize an event for the VelocityTracker.\r
+            final long time = SystemClock.uptimeMillis();\r
+            ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,\r
+                    mLastMotionX, 0, 0);\r
+        } else {\r
+            mLastMotionY += yOffset;\r
+\r
+            float oldScrollY = getScrollY();\r
+            float scrollY = oldScrollY - yOffset;\r
+            final int height = getClientHeight();\r
+\r
+            float topBound = height * mFirstOffset;\r
+            float bottomBound = height * mLastOffset;\r
+\r
+            final ItemInfo firstItem = mItems.get(0);\r
+            final ItemInfo lastItem = mItems.get(mItems.size() - 1);\r
+            if (firstItem.position != 0) {\r
+                topBound = firstItem.offset * height;\r
+            }\r
+            if (lastItem.position != mAdapter.getCount() - 1) {\r
+                bottomBound = lastItem.offset * height;\r
+            }\r
+\r
+            if (scrollY < topBound) {\r
+                scrollY = topBound;\r
+            } else if (scrollY > bottomBound) {\r
+                scrollY = bottomBound;\r
+            }\r
+            // Don't lose the rounded component\r
+            mLastMotionY += scrollY - (int) scrollY;\r
+            scrollTo(getScrollX(), (int) scrollY);\r
+            pageScrolled(0, (int) scrollY);\r
+\r
+            // Synthesize an event for the VelocityTracker.\r
+            final long time = SystemClock.uptimeMillis();\r
+            ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,\r
+                    0, mLastMotionY, 0);\r
+        }\r
+        mVelocityTracker.addMovement(ev);\r
+        ev.recycle();\r
+    }\r
+\r
+    /**\r
+     * Returns true if a fake drag is in progress.\r
+     *\r
+     * @return true if currently in a fake drag, false otherwise.\r
+     * @see #beginFakeDrag()\r
+     * @see #fakeDragBy(float)\r
+     * @see #endFakeDrag()\r
+     */\r
+    public boolean isFakeDragging() {\r
+        return mFakeDragging;\r
+    }\r
+\r
+    private void onSecondaryPointerUp(MotionEvent ev) {\r
+        final int pointerIndex = MotionEventCompat.getActionIndex(ev);\r
+        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);\r
+        if (pointerId == mActivePointerId) {\r
+            // This was our active pointer going up. Choose a new\r
+            // active pointer and adjust accordingly.\r
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;\r
+            if (isHorizontal()) {\r
+                mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);\r
+            } else {\r
+                mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);\r
+            }\r
+            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);\r
+            if (mVelocityTracker != null) {\r
+                mVelocityTracker.clear();\r
+            }\r
+        }\r
+    }\r
+\r
+    private void endDrag() {\r
+        mIsBeingDragged = false;\r
+        mIsUnableToDrag = false;\r
+\r
+        if (mVelocityTracker != null) {\r
+            mVelocityTracker.recycle();\r
+            mVelocityTracker = null;\r
+        }\r
+    }\r
+\r
+    private void setScrollingCacheEnabled(boolean enabled) {\r
+        if (mScrollingCacheEnabled != enabled) {\r
+            mScrollingCacheEnabled = enabled;\r
+            if (USE_CACHE) {\r
+                final int size = getChildCount();\r
+                for (int i = 0; i < size; ++i) {\r
+                    final View child = getChildAt(i);\r
+                    if (child.getVisibility() != GONE) {\r
+                        child.setDrawingCacheEnabled(enabled);\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    public boolean canScrollHorizontally(int direction) {\r
+        if (mAdapter == null) {\r
+            return false;\r
+        }\r
+\r
+        final int width = getClientWidth();\r
+        final int scrollX = getScrollX();\r
+        if (direction < 0) {\r
+            return (scrollX > (int) (width * mFirstOffset));\r
+        } else if (direction > 0) {\r
+            return (scrollX < (int) (width * mLastOffset));\r
+        } else {\r
+            return false;\r
+        }\r
+    }\r
+\r
+    public boolean internalCanScrollVertically(int direction) {\r
+        if (mAdapter == null) {\r
+            return false;\r
+        }\r
+\r
+        final int height = getClientHeight();\r
+        final int scrollY = getScrollY();\r
+        if (direction < 0) {\r
+            return (scrollY > (int) (height * mFirstOffset));\r
+        } else if (direction > 0) {\r
+            return (scrollY < (int) (height * mLastOffset));\r
+        } else {\r
+            return false;\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Tests scrollability within child views of v given a delta of dx.\r
+     *\r
+     * @param v      View to test for horizontal scrollability\r
+     * @param checkV Whether the view v passed should itself be checked for scrollability (true),\r
+     *               or just its children (false).\r
+     * @param dx     Delta scrolled in pixels\r
+     * @param x      X coordinate of the active touch point\r
+     * @param y      Y coordinate of the active touch point\r
+     * @return true if child views of v can be scrolled by delta of dx.\r
+     */\r
+    protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) {\r
+        if (v instanceof ViewGroup) {\r
+            if (isHorizontal()) {\r
+                final ViewGroup group = (ViewGroup) v;\r
+                final int scrollX = v.getScrollX();\r
+                final int scrollY = v.getScrollY();\r
+                final int count = group.getChildCount();\r
+                // Count backwards - let topmost views consume scroll distance first.\r
+                for (int i = count - 1; i >= 0; i--) {\r
+                    // TODO: Add versioned support here for transformed views.\r
+                    // This will not work for transformed views in Honeycomb+\r
+                    final View child = group.getChildAt(i);\r
+                    if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&\r
+                            y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&\r
+                            canScroll(child, true, dx, 0, x + scrollX - child.getLeft(),\r
+                                    y + scrollY - child.getTop())) {\r
+                        return true;\r
+                    }\r
+                }\r
+                return checkV && ViewCompat.canScrollHorizontally(v, -dx);\r
+            } else {\r
+                final ViewGroup group = (ViewGroup) v;\r
+                final int scrollX = v.getScrollX();\r
+                final int scrollY = v.getScrollY();\r
+                final int count = group.getChildCount();\r
+                // Count backwards - let topmost views consume scroll distance first.\r
+                for (int i = count - 1; i >= 0; i--) {\r
+                    // TODO: Add versioned support here for transformed views.\r
+                    // This will not work for transformed views in Honeycomb+\r
+                    final View child = group.getChildAt(i);\r
+                    if (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&\r
+                            x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&\r
+                            canScroll(child, true, 0, dy, x + scrollX - child.getLeft(),\r
+                                    y + scrollY - child.getTop())) {\r
+                        return true;\r
+                    }\r
+                }\r
+\r
+                return checkV && ViewCompat.canScrollVertically(v, -dy);\r
+\r
+            }\r
+        }\r
+        return false;\r
+    }\r
+\r
+    @Override\r
+    public boolean dispatchKeyEvent(KeyEvent event) {\r
+        // Let the focused view and/or our descendants get the key first\r
+        return super.dispatchKeyEvent(event) || executeKeyEvent(event);\r
+    }\r
+\r
+    /**\r
+     * You can call this function yourself to have the scroll view perform\r
+     * scrolling from a key event, just as if the event had been dispatched to\r
+     * it by the view hierarchy.\r
+     *\r
+     * @param event The key event to execute.\r
+     * @return Return true if the event was handled, else false.\r
+     */\r
+    public boolean executeKeyEvent(KeyEvent event) {\r
+        boolean handled = false;\r
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {\r
+            switch (event.getKeyCode()) {\r
+                case KeyEvent.KEYCODE_DPAD_LEFT:\r
+                    handled = arrowScroll(FOCUS_LEFT);\r
+                    break;\r
+                case KeyEvent.KEYCODE_DPAD_RIGHT:\r
+                    handled = arrowScroll(FOCUS_RIGHT);\r
+                    break;\r
+                case KeyEvent.KEYCODE_TAB:\r
+                    if (Build.VERSION.SDK_INT >= 11) {\r
+                        // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD\r
+                        // before Android 3.0. Ignore the tab key on those devices.\r
+                        if (KeyEvent.metaStateHasNoModifiers(event.getMetaState())) {\r
+                            handled = arrowScroll(FOCUS_FORWARD);\r
+                        } else if (KeyEvent.metaStateHasNoModifiers(event.getMetaState())) {\r
+                            handled = arrowScroll(FOCUS_BACKWARD);\r
+                        }\r
+                    }\r
+                    break;\r
+            }\r
+        }\r
+        return handled;\r
+    }\r
+\r
+    public boolean arrowScroll(int direction) {\r
+        View currentFocused = findFocus();\r
+        if (currentFocused == this) {\r
+            currentFocused = null;\r
+        } else if (currentFocused != null) {\r
+            boolean isChild = false;\r
+            for (ViewParent parent\r
+                    = currentFocused.getParent();\r
+                        parent instanceof ViewGroup;\r
+                            parent = parent.getParent()) {\r
+                if (parent == this) {\r
+                    isChild = true;\r
+                    break;\r
+                }\r
+            }\r
+            if (!isChild) {\r
+                // This would cause the focus search down below to fail in fun ways.\r
+                final StringBuilder sb = new StringBuilder();\r
+                sb.append(currentFocused.getClass().getSimpleName());\r
+                for (ViewParent parent\r
+                        = currentFocused.getParent();\r
+                                parent instanceof ViewGroup;\r
+                                   parent = parent.getParent()) {\r
+                    sb.append(" => ").append(parent.getClass().getSimpleName());\r
+                }\r
+                Log.e(TAG, "arrowScroll tried to find focus based on non-child " +\r
+                        "current focused view " + sb.toString());\r
+                currentFocused = null;\r
+            }\r
+        }\r
+\r
+        boolean handled = false;\r
+\r
+        View nextFocused\r
+                    = FocusFinder.getInstance().findNextFocus(this, currentFocused,\r
+                        direction);\r
+        if (nextFocused != null && nextFocused != currentFocused) {\r
+            if (isHorizontal()) {\r
+                if (direction == View.FOCUS_LEFT) {\r
+                    // If there is nothing\r
+                    // to the left, or this is causing us to\r
+                    // jump to the right,\r
+                    // then what we really want to do is page left.\r
+                    final int nextLeft\r
+                            = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;\r
+                    final int currLeft\r
+                            = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;\r
+                    if (currentFocused != null && nextLeft >= currLeft) {\r
+                        handled = pageLeft();\r
+                    } else {\r
+                        handled = nextFocused.requestFocus();\r
+                    }\r
+                } else if (direction == View.FOCUS_RIGHT) {\r
+                    // If there is nothing to the right,\r
+                    // or this is causing us to\r
+                    // jump to the left,\r
+                    // then what we really\r
+                    // want to do is page right.\r
+                    final int nextLeft\r
+                            = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;\r
+                    final int currLeft\r
+                            = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;\r
+                    if (currentFocused != null && nextLeft <= currLeft) {\r
+                        handled = pageRight();\r
+                    } else {\r
+                        handled = nextFocused.requestFocus();\r
+                    }\r
+                } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {\r
+                    // Trying to move left and nothing there; try to page.\r
+                    handled = pageLeft();\r
+                } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {\r
+                    // Trying to move right and nothing there; try to page.\r
+                    handled = pageRight();\r
+                }\r
+            } else {\r
+                if (direction == View.FOCUS_UP) {\r
+                    // If there is nothing to the left,\r
+                    // or this is causing us to\r
+                    // jump to the right,\r
+                    // then what we really want to do is page left.\r
+                    final int nextTop\r
+                            = getChildRectInPagerCoordinates(mTempRect, nextFocused).top;\r
+                    final int currTop\r
+                            = getChildRectInPagerCoordinates(mTempRect, currentFocused).top;\r
+                    if (currentFocused != null && nextTop >= currTop) {\r
+                        handled = pageUp();\r
+                    } else {\r
+                        handled = nextFocused.requestFocus();\r
+                    }\r
+                } else if (direction == View.FOCUS_DOWN) {\r
+                    final int nextDown =\r
+                            getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom;\r
+                    final int currDown =\r
+                            getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom;\r
+                    if (currentFocused != null && nextDown <= currDown) {\r
+                        handled = pageDown();\r
+                    } else {\r
+                        handled = nextFocused.requestFocus();\r
+                    }\r
+                } else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) {\r
+                    // Trying to move left and nothing there; try to page.\r
+                    handled = pageUp();\r
+                } else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) {\r
+                    // Trying to move right and nothing there; try to page.\r
+                    handled = pageDown();\r
+\r
+                }\r
+            }\r
+            if (handled) {\r
+                playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));\r
+            }\r
+            return handled;\r
+        }\r
+        return handled;\r
+    }\r
+\r
+\r
+    private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {\r
+        if (outRect == null) {\r
+            outRect = new Rect();\r
+        }\r
+        if (child == null) {\r
+            outRect.set(0, 0, 0, 0);\r
+            return outRect;\r
+        }\r
+        outRect.left = child.getLeft();\r
+        outRect.right = child.getRight();\r
+        outRect.top = child.getTop();\r
+        outRect.bottom = child.getBottom();\r
+\r
+        ViewParent parent = child.getParent();\r
+        while (parent instanceof ViewGroup && parent != this) {\r
+            final ViewGroup group = (ViewGroup) parent;\r
+            outRect.left += group.getLeft();\r
+            outRect.right += group.getRight();\r
+            outRect.top += group.getTop();\r
+            outRect.bottom += group.getBottom();\r
+\r
+            parent = group.getParent();\r
+        }\r
+        return outRect;\r
+    }\r
+\r
+    boolean pageLeft() {\r
+        if (mCurItem > 0) {\r
+            setCurrentItem(mCurItem - 1, true);\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    boolean pageRight() {\r
+        if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) {\r
+            setCurrentItem(mCurItem + 1, true);\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    boolean pageUp() {\r
+        if (mCurItem > 0) {\r
+            setCurrentItem(mCurItem - 1, true);\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    boolean pageDown() {\r
+        if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) {\r
+            setCurrentItem(mCurItem + 1, true);\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * We only want the current page that is being shown to be focusable.\r
+     */\r
+    @Override\r
+    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {\r
+        final int focusableCount = views.size();\r
+\r
+        final int descendantFocusability = getDescendantFocusability();\r
+\r
+        if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {\r
+            for (int i = 0; i < getChildCount(); i++) {\r
+                final View child = getChildAt(i);\r
+                if (child.getVisibility() == VISIBLE) {\r
+                    ItemInfo ii = infoForChild(child);\r
+                    if (ii != null && ii.position == mCurItem) {\r
+                        child.addFocusables(views, direction, focusableMode);\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        // we add ourselves (if focusable) in all cases except for when we are\r
+        // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is\r
+        // to avoid the focus search finding layouts when a more precise search\r
+        // among the focusable children would be more interesting.\r
+        if (\r
+                descendantFocusability != FOCUS_AFTER_DESCENDANTS ||\r
+                        // No focusable descendants\r
+                        (focusableCount == views.size())) {\r
+            // Note that we can't call the superclass here, because it will\r
+            // add all views in.  So we need to do the same thing View does.\r
+            if (!isFocusable()) {\r
+                return;\r
+            }\r
+            if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&\r
+                    isInTouchMode() && !isFocusableInTouchMode()) {\r
+                return;\r
+            }\r
+            if (views != null) {\r
+                views.add(this);\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * We only want the current page that is being shown to be touchable.\r
+     */\r
+    @Override\r
+    public void addTouchables(ArrayList<View> views) {\r
+        // Note that we don't call super.addTouchables(), which means that\r
+        // we don't call View.addTouchables().  This is okay because a ViewPager\r
+        // is itself not touchable.\r
+        for (int i = 0; i < getChildCount(); i++) {\r
+            final View child = getChildAt(i);\r
+            if (child.getVisibility() == VISIBLE) {\r
+                ItemInfo ii = infoForChild(child);\r
+                if (ii != null && ii.position == mCurItem) {\r
+                    child.addTouchables(views);\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * We only want the current page that is being shown to be focusable.\r
+     */\r
+    @Override\r
+    protected boolean onRequestFocusInDescendants(int direction,\r
+                                                  Rect previouslyFocusedRect) {\r
+        int index;\r
+        int increment;\r
+        int end;\r
+        int count = getChildCount();\r
+        if ((direction & FOCUS_FORWARD) != 0) {\r
+            index = 0;\r
+            increment = 1;\r
+            end = count;\r
+        } else {\r
+            index = count - 1;\r
+            increment = -1;\r
+            end = -1;\r
+        }\r
+        for (int i = index; i != end; i += increment) {\r
+            View child = getChildAt(i);\r
+            if (child.getVisibility() == VISIBLE) {\r
+                ItemInfo ii = infoForChild(child);\r
+                if (ii != null && ii.position ==\r
+                            mCurItem && child.requestFocus(direction, previouslyFocusedRect)) {\r
+                    return true;\r
+                }\r
+            }\r
+        }\r
+        return false;\r
+    }\r
+\r
+    @Override\r
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {\r
+        // Dispatch scroll events from this ViewPager.\r
+        if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) {\r
+            return super.dispatchPopulateAccessibilityEvent(event);\r
+        }\r
+\r
+        // Dispatch all other accessibility events from the current page.\r
+        final int childCount = getChildCount();\r
+        for (int i = 0; i < childCount; i++) {\r
+            final View child = getChildAt(i);\r
+            if (child.getVisibility() == VISIBLE) {\r
+                final ItemInfo ii = infoForChild(child);\r
+                if (ii != null && ii.position == mCurItem &&\r
+                        child.dispatchPopulateAccessibilityEvent(event)) {\r
+                    return true;\r
+                }\r
+            }\r
+        }\r
+\r
+        return false;\r
+    }\r
+\r
+    @Override\r
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {\r
+        return new LayoutParams();\r
+    }\r
+\r
+    @Override\r
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {\r
+        return generateDefaultLayoutParams();\r
+    }\r
+\r
+    @Override\r
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {\r
+        return p instanceof LayoutParams && super.checkLayoutParams(p);\r
+    }\r
+\r
+    @Override\r
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {\r
+        return new LayoutParams(getContext(), attrs);\r
+    }\r
+\r
+    class MyAccessibilityDelegate extends AccessibilityDelegateCompat {\r
+\r
+        @Override\r
+        public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {\r
+            super.onInitializeAccessibilityEvent(host, event);\r
+            event.setClassName(DirectionalViewpager.class.getName());\r
+            AccessibilityRecordCompat recordCompat = null;\r
+            if (isHorizontal()) {\r
+                recordCompat =\r
+                        AccessibilityEventCompat.asRecord(event);\r
+            } else {\r
+                recordCompat = AccessibilityRecordCompat.obtain();\r
+            }\r
+            recordCompat.setScrollable(canScroll());\r
+            if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED\r
+                    && mAdapter != null) {\r
+                recordCompat.setItemCount(mAdapter.getCount());\r
+                recordCompat.setFromIndex(mCurItem);\r
+                recordCompat.setToIndex(mCurItem);\r
+            }\r
+        }\r
+\r
+        @Override\r
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {\r
+            super.onInitializeAccessibilityNodeInfo(host, info);\r
+            info.setClassName(DirectionalViewpager.class.getName());\r
+            info.setScrollable(canScroll());\r
+            if (isHorizontal()) {\r
+                if (canScrollHorizontally(1)) {\r
+                    info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);\r
+                }\r
+                if (canScrollHorizontally(-1)) {\r
+                    info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);\r
+                }\r
+            } else {\r
+                if (internalCanScrollVertically(1)) {\r
+                    info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);\r
+                }\r
+                if (internalCanScrollVertically(-1)) {\r
+                    info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);\r
+                }\r
+            }\r
+        }\r
+\r
+        @Override\r
+        public boolean performAccessibilityAction(View host, int action, Bundle args) {\r
+            if (super.performAccessibilityAction(host, action, args)) {\r
+                return true;\r
+            }\r
+\r
+            if (isHorizontal()) {\r
+                switch (action) {\r
+                    case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {\r
+                        if (canScrollHorizontally(1)) {\r
+                            setCurrentItem(mCurItem + 1);\r
+                            return true;\r
+                        }\r
+                    }\r
+                    return false;\r
+                    case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {\r
+                        if (canScrollHorizontally(-1)) {\r
+                            setCurrentItem(mCurItem - 1);\r
+                            return true;\r
+                        }\r
+                    }\r
+                    return false;\r
+                }\r
+            } else {\r
+                switch (action) {\r
+                    case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {\r
+                        if (internalCanScrollVertically(1)) {\r
+                            setCurrentItem(mCurItem + 1);\r
+                            return true;\r
+                        }\r
+                    }\r
+                    return false;\r
+                    case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {\r
+                        if (internalCanScrollVertically(-1)) {\r
+                            setCurrentItem(mCurItem - 1);\r
+                            return true;\r
+                        }\r
+                    }\r
+                    return false;\r
+                }\r
+            }\r
+            return false;\r
+        }\r
+\r
+        private boolean canScroll() {\r
+            return (mAdapter != null) && (mAdapter.getCount() > 1);\r
+        }\r
+    }\r
+\r
+    private class PagerObserver extends DataSetObserver {\r
+        @Override\r
+        public void onChanged() {\r
+            dataSetChanged();\r
+        }\r
+\r
+        @Override\r
+        public void onInvalidated() {\r
+            dataSetChanged();\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Layout parameters that should be supplied for views added to a\r
+     * ViewPager.\r
+     */\r
+    public static class LayoutParams extends ViewGroup.LayoutParams {\r
+        /**\r
+         * true if this view is a decoration on the pager itself and not\r
+         * a view supplied by the adapter.\r
+         */\r
+        public boolean isDecor;\r
+\r
+        /**\r
+         * Gravity setting for use on decor views only:\r
+         * Where to position the view page within the overall ViewPager\r
+         * container; constants are defined in {@link android.view.Gravity}.\r
+         */\r
+        public int gravity;\r
+\r
+        /**\r
+         * Width as a 0-1 multiplier of the measured pager width\r
+         */\r
+        float widthFactor = 0.f;\r
+\r
+        float heightFactor = 0.f;\r
+\r
+        /**\r
+         * true if this view was added during layout and needs to be measured\r
+         * before being positioned.\r
+         */\r
+        boolean needsMeasure;\r
+\r
+        /**\r
+         * Adapter position this view is for if !isDecor\r
+         */\r
+        int position;\r
+\r
+        /**\r
+         * Current child index within the ViewPager that this view occupies\r
+         */\r
+        int childIndex;\r
+\r
+        public LayoutParams() {\r
+            super(FILL_PARENT, FILL_PARENT);\r
+        }\r
+\r
+        public LayoutParams(Context context, AttributeSet attrs) {\r
+            super(context, attrs);\r
+\r
+            final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);\r
+            gravity = a.getInteger(0, Gravity.TOP);\r
+            a.recycle();\r
+        }\r
+    }\r
+\r
+    static class ViewPositionComparator implements Comparator<View> {\r
+        @Override\r
+        public int compare(View lhs, View rhs) {\r
+            final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();\r
+            final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();\r
+            if (llp.isDecor != rlp.isDecor) {\r
+                return llp.isDecor ? 1 : -1;\r
+            }\r
+            return llp.position - rlp.position;\r
+        }\r
+    }\r
+\r
+    public void setDirection(Direction direction) {\r
+        mDirection = direction.name();\r
+        initViewPager();\r
+    }\r
+\r
+    private String logDestroyItem(int pos, View object) {\r
+        return "populate() - destroyItem() with pos: " + pos + " view: " + object;\r
+    }\r
+}\r
diff --git a/Android/folioreader/src/main/java/com/folioreader/view/ObservableWebView.java b/Android/folioreader/src/main/java/com/folioreader/view/ObservableWebView.java
new file mode 100755 (executable)
index 0000000..afae6c3
--- /dev/null
@@ -0,0 +1,172 @@
+package com.folioreader.view;\r
+\r
+import android.annotation.TargetApi;\r
+import android.content.Context;\r
+import android.os.Build;\r
+import android.util.AttributeSet;\r
+import android.view.ActionMode;\r
+import android.view.Menu;\r
+import android.view.MenuInflater;\r
+import android.view.MotionEvent;\r
+import android.view.View;\r
+import android.webkit.WebView;\r
+\r
+/**\r
+ * Created by mahavir on 3/31/16.\r
+ */\r
+public class ObservableWebView extends WebView {\r
+\r
+    private float mDownPosX = 0;\r
+    private float mDownPosY = 0;\r
+\r
+    public interface ScrollListener {\r
+        void onScrollChange(int percent);\r
+    }\r
+\r
+    public interface SeekBarListener {\r
+        void fadeInSeekBarIfInvisible();\r
+    }\r
+\r
+    public interface ToolBarListener {\r
+        void hideOrshowToolBar();\r
+        void hideToolBarIfVisible();\r
+    }\r
+\r
+    private ScrollListener mScrollListener;\r
+    private SeekBarListener mSeekBarListener;\r
+    private ToolBarListener mToolBarListener;\r
+\r
+    public ObservableWebView(Context context) {\r
+        super(context);\r
+    }\r
+\r
+    public ObservableWebView(Context context, AttributeSet attrs) {\r
+        super(context, attrs);\r
+    }\r
+\r
+    public ObservableWebView(Context context, AttributeSet attrs, int defStyleAttr) {\r
+        super(context, attrs, defStyleAttr);\r
+    }\r
+\r
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\r
+    public ObservableWebView(Context context, AttributeSet attrs,\r
+                             int defStyleAttr, int defStyleRes) {\r
+        super(context, attrs, defStyleAttr, defStyleRes);\r
+    }\r
+\r
+    public void setScrollListener(ScrollListener listener) {\r
+        mScrollListener = listener;\r
+    }\r
+\r
+    public void setSeekBarListener(SeekBarListener listener) {\r
+        mSeekBarListener = listener;\r
+    }\r
+\r
+    public void setToolBarListener(ToolBarListener listener) {\r
+        mToolBarListener = listener;\r
+    }\r
+\r
+    @Override\r
+    public boolean onTouchEvent(MotionEvent event) {\r
+        final int action = event.getAction();\r
+        float MOVE_THRESHOLD_DP = 20 * getResources().getDisplayMetrics().density;\r
+\r
+        switch (action) {\r
+            case MotionEvent.ACTION_DOWN:\r
+                mDownPosX = event.getX();\r
+                mDownPosY = event.getY();\r
+                if (mSeekBarListener != null) mSeekBarListener.fadeInSeekBarIfInvisible();\r
+                break;\r
+            case MotionEvent.ACTION_UP:\r
+                if (mToolBarListener != null &&\r
+                        (Math.abs(event.getX() - mDownPosX) < MOVE_THRESHOLD_DP\r
+                                || Math.abs(event.getY() - mDownPosY) < MOVE_THRESHOLD_DP)) {\r
+                    mToolBarListener.hideOrshowToolBar();\r
+                }\r
+                break;\r
+        }\r
+        return super.onTouchEvent(event);\r
+    }\r
+\r
+    @Override\r
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {\r
+        if (mToolBarListener != null) mToolBarListener.hideToolBarIfVisible();\r
+        if (mScrollListener != null) mScrollListener.onScrollChange(t);\r
+        super.onScrollChanged(l, t, oldl, oldt);\r
+    }\r
+\r
+    public int getContentHeightVal() {\r
+        return (int) Math.floor(this.getContentHeight() * this.getScale());\r
+    }\r
+\r
+    public int getWebViewHeight() {\r
+        return this.getMeasuredHeight();\r
+    }\r
+\r
+    @Override\r
+    public ActionMode startActionMode(ActionMode.Callback callback, int type) {\r
+        return this.dummyActionMode();\r
+    }\r
+\r
+    @Override\r
+    public ActionMode startActionMode(ActionMode.Callback callback) {\r
+        return this.dummyActionMode();\r
+    }\r
+\r
+    public ActionMode dummyActionMode() {\r
+        return new ActionMode() {\r
+            @Override\r
+            public void setTitle(CharSequence title) {\r
+            }\r
+\r
+            @Override\r
+            public void setTitle(int resId) {\r
+            }\r
+\r
+            @Override\r
+            public void setSubtitle(CharSequence subtitle) {\r
+            }\r
+\r
+            @Override\r
+            public void setSubtitle(int resId) {\r
+            }\r
+\r
+            @Override\r
+            public void setCustomView(View view) {\r
+            }\r
+\r
+            @Override\r
+            public void invalidate() {\r
+            }\r
+\r
+            @Override\r
+            public void finish() {\r
+            }\r
+\r
+            @Override\r
+            public Menu getMenu() {\r
+                return null;\r
+            }\r
+\r
+            @Override\r
+            public CharSequence getTitle() {\r
+                return null;\r
+            }\r
+\r
+            @Override\r
+            public CharSequence getSubtitle() {\r
+                return null;\r
+            }\r
+\r
+            @Override\r
+            public View getCustomView() {\r
+                return null;\r
+            }\r
+\r
+            @Override\r
+            public MenuInflater getMenuInflater() {\r
+                return null;\r
+            }\r
+        };\r
+    }\r
+}\r
diff --git a/Android/folioreader/src/main/java/com/folioreader/view/StyleableTextView.java b/Android/folioreader/src/main/java/com/folioreader/view/StyleableTextView.java
new file mode 100755 (executable)
index 0000000..4435f2a
--- /dev/null
@@ -0,0 +1,30 @@
+package com.folioreader.view;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatTextView;
+import android.util.AttributeSet;
+
+import com.folioreader.R;
+import com.folioreader.util.UiUtil;
+
+public class StyleableTextView extends AppCompatTextView {
+
+    public StyleableTextView(Context context) {
+        super(context);
+    }
+
+    public StyleableTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        UiUtil.setCustomFont(this, context, attrs,
+                R.styleable.StyleableTextView,
+                R.styleable.StyleableTextView_folio_font);
+    }
+
+    public StyleableTextView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        UiUtil.setCustomFont(this, context, attrs,
+                R.styleable.StyleableTextView,
+                R.styleable.StyleableTextView_folio_font);
+    }
+
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/view/UnderlinedTextView.java b/Android/folioreader/src/main/java/com/folioreader/view/UnderlinedTextView.java
new file mode 100755 (executable)
index 0000000..12ce69d
--- /dev/null
@@ -0,0 +1,107 @@
+package com.folioreader.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.support.v7.widget.AppCompatTextView;
+import android.text.Layout;
+import android.util.AttributeSet;
+
+import com.folioreader.R;
+
+/**
+ * Created by mobisys on 7/4/2016.
+ */
+public class UnderlinedTextView extends AppCompatTextView {
+
+    private Rect mRect;
+    private Paint mPaint;
+    private int mColor;
+    private float mDensity;
+    private float mStrokeWidth;
+
+    public UnderlinedTextView(Context context) {
+        this(context, null, 0);
+    }
+
+    public UnderlinedTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public UnderlinedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs, defStyleAttr);
+    }
+
+    private void init(Context context, AttributeSet attributeSet, int defStyle) {
+
+        mDensity = context.getResources().getDisplayMetrics().density;
+
+        TypedArray typedArray =
+                context.obtainStyledAttributes(attributeSet, R.styleable.UnderlinedTextView,
+                        defStyle, 0);
+        mStrokeWidth =
+                typedArray.getDimension(
+                        R.styleable.UnderlinedTextView_underlineWidth,
+                        mDensity * 2);
+        typedArray.recycle();
+
+        mRect = new Rect();
+        mPaint = new Paint();
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setColor(mColor); //line mColor
+        mPaint.setStrokeWidth(mStrokeWidth);
+    }
+
+    public int getUnderLineColor() {
+        return mColor;
+    }
+
+    public void setUnderLineColor(int mColor) {
+        this.mColor = mColor;
+        mRect = new Rect();
+        mPaint = new Paint();
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setColor(mColor); //line mColor
+        mPaint.setStrokeWidth(mStrokeWidth);
+        postInvalidate();
+    }
+
+    public float getUnderlineWidth() {
+        return mStrokeWidth;
+    }
+
+    public void setUnderlineWidth(float mStrokeWidth) {
+        this.mStrokeWidth = mStrokeWidth;
+        postInvalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        int count = getLineCount();
+
+        final Layout layout = getLayout();
+        float xStart, xStop, xDiff;
+        int firstCharInLine, lastCharInLine;
+
+        for (int i = 0; i < count; i++) {
+            int baseline = getLineBounds(i, mRect);
+            firstCharInLine = layout.getLineStart(i);
+            lastCharInLine = layout.getLineEnd(i);
+
+            xStart = layout.getPrimaryHorizontal(firstCharInLine);
+            xDiff = layout.getPrimaryHorizontal(firstCharInLine + 1) - xStart;
+            xStop = layout.getPrimaryHorizontal(lastCharInLine - 1) + xDiff;
+
+            canvas.drawLine(xStart,
+                    baseline + mStrokeWidth,
+                    xStop,
+                    baseline + mStrokeWidth,
+                    mPaint);
+        }
+
+        super.onDraw(canvas);
+    }
+}
\ No newline at end of file
diff --git a/Android/folioreader/src/main/java/com/folioreader/view/VerticalSeekbar.java b/Android/folioreader/src/main/java/com/folioreader/view/VerticalSeekbar.java
new file mode 100755 (executable)
index 0000000..62d1ab8
--- /dev/null
@@ -0,0 +1,123 @@
+package com.folioreader.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.support.v7.widget.AppCompatSeekBar;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+/**
+ * Created by priyank on 4/19/16.
+ */
+
+public class VerticalSeekbar extends AppCompatSeekBar {
+
+    public VerticalSeekbar(Context context) {
+        super(context);
+    }
+
+    public VerticalSeekbar(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public VerticalSeekbar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(h, w, oldh, oldw);
+    }
+
+    @Override
+    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(heightMeasureSpec, widthMeasureSpec);
+        setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
+    }
+
+    protected void onDraw(Canvas c) {
+        c.rotate(90);
+        c.translate(0, -getWidth());
+
+        super.onDraw(c);
+    }
+
+    private OnSeekBarChangeListener mOnChangeListener;
+
+    @Override
+    public void setOnSeekBarChangeListener(OnSeekBarChangeListener mOnChangeListener) {
+        this.mOnChangeListener = mOnChangeListener;
+    }
+
+    private int mLastProgress = 0;
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!isEnabled()) {
+            return false;
+        }
+
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                if (mOnChangeListener != null)
+                    mOnChangeListener.onStartTrackingTouch(this);
+                setPressed(true);
+                setSelected(true);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                super.onTouchEvent(event);
+                int progress = getMax() - (int) (getMax() * event.getY() / getHeight());
+
+                // Ensure progress stays within boundaries
+                if (progress < 0) {
+                    progress = 0;
+                }
+                if (progress > getMax()) {
+                    progress = getMax();
+                }
+                setProgress(progress);  // Draw progress
+                if (progress != mLastProgress) {
+                    // Only enact listener if the progress has actually changed
+                    mLastProgress = progress;
+                    if (mOnChangeListener != null)
+                        mOnChangeListener.onProgressChanged(this, progress, true);
+                }
+
+                onSizeChanged(getWidth(), getHeight(), 0, 0);
+                setPressed(true);
+                setSelected(true);
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mOnChangeListener != null)
+                    mOnChangeListener.onStopTrackingTouch(this);
+                setPressed(false);
+                setSelected(false);
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                super.onTouchEvent(event);
+                setPressed(false);
+                setSelected(false);
+                break;
+        }
+        return true;
+    }
+
+    public synchronized void setProgressAndThumb(int progress) {
+        setProgress(progress);
+        onSizeChanged(getWidth(), getHeight(), 0, 0);
+        if (progress != mLastProgress) {
+            // Only enact listener if the progress has actually changed
+            mLastProgress = progress;
+            if (mOnChangeListener != null)
+                mOnChangeListener.onProgressChanged(this, progress, true);
+        }
+    }
+
+    public synchronized void setMaximum(int maximum) {
+        setMax(maximum);
+    }
+
+    public synchronized int getMaximum() {
+        return getMax();
+    }
+
+}
diff --git a/Android/folioreader/src/main/java/com/folioreader/view/VerticalViewPager.java b/Android/folioreader/src/main/java/com/folioreader/view/VerticalViewPager.java
new file mode 100755 (executable)
index 0000000..8fe73b2
--- /dev/null
@@ -0,0 +1,2861 @@
+package com.folioreader.view;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.v4.os.ParcelableCompat;
+import android.support.v4.os.ParcelableCompatCreatorCallbacks;
+import android.support.v4.view.AccessibilityDelegateCompat;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.VelocityTrackerCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewConfigurationCompat;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.support.v4.view.ViewPager.PageTransformer;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
+import android.support.v4.widget.EdgeEffectCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.FocusFinder;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.Interpolator;
+import android.widget.Scroller;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Created by castorflex on 12/29/13.
+ * Just a copy of the original ViewPager modified to support vertical Scrolling
+ */
+public class VerticalViewPager extends ViewGroup {
+    private static final String TAG = "ViewPager";
+    private static final boolean DEBUG = false;
+
+    private static final boolean USE_CACHE = false;
+
+    private static final int DEFAULT_OFFSCREEN_PAGES = 1;
+    private static final int MAX_SETTLE_DURATION = 600; // ms
+    private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
+
+    private static final int DEFAULT_GUTTER_SIZE = 16; // dips
+
+    private static final int MIN_FLING_VELOCITY = 400; // dips
+
+    private static final int[] LAYOUT_ATTRS = new int[]{
+            android.R.attr.layout_gravity
+    };
+
+
+
+    /**
+     * Used to track what the expected number of items in the adapter should be.
+     * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
+     */
+    private int mExpectedAdapterCount;
+
+    static class ItemInfo {
+        Object object;
+        int position;
+        boolean scrolling;
+        float heightFactor;
+
+        float offset;
+    }
+
+    private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>() {
+        @Override
+        public int compare(ItemInfo lhs, ItemInfo rhs) {
+            return lhs.position - rhs.position;
+        }
+    };
+
+    private static final Interpolator sInterpolator = new Interpolator() {
+        public float getInterpolation(float t) {
+            t -= 1.0f;
+            return t * t * t * t * t + 1.0f;
+        }
+    };
+
+    private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
+    private final ItemInfo mTempItem = new ItemInfo();
+
+    private final Rect mTempRect = new Rect();
+
+    private PagerAdapter mAdapter;
+    private int mCurItem;   // Index of currently displayed page.
+    private int mRestoredCurItem = -1;
+    private Parcelable mRestoredAdapterState = null;
+    private ClassLoader mRestoredClassLoader = null;
+    private Scroller mScroller;
+    private PagerObserver mObserver;
+
+    private int mPageMargin;
+    private Drawable mMarginDrawable;
+    private int mLeftPageBounds;
+    private int mRightPageBounds;
+
+    // Offsets of the first and last items, if known.
+    // Set during population, used to determine if we are at the beginning
+    // or end of the pager data set during touch scrolling.
+    private float mFirstOffset = -Float.MAX_VALUE;
+    private float mLastOffset = Float.MAX_VALUE;
+
+    private int mChildWidthMeasureSpec;
+    private int mChildHeightMeasureSpec;
+    private boolean mInLayout;
+
+    private boolean mScrollingCacheEnabled;
+
+    private boolean mPopulatePending;
+    private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
+
+    private boolean mIsBeingDragged;
+    private boolean mIsUnableToDrag;
+    private boolean mIgnoreGutter;
+    private int mDefaultGutterSize;
+    private int mGutterSize;
+    private int mTouchSlop;
+    /**
+     * Position of the last motion event.
+     */
+    private float mLastMotionX;
+    private float mLastMotionY;
+    private float mInitialMotionX;
+    private float mInitialMotionY;
+    /**
+     * ID of the active pointer. This is used to retain consistency during
+     * drags/flings if multiple pointers are used.
+     */
+    private int mActivePointerId = INVALID_POINTER;
+    /**
+     * Sentinel value for no current active pointer.
+     * Used by {@link #mActivePointerId}.
+     */
+    private static final int INVALID_POINTER = -1;
+
+    /**
+     * Determines speed during touch scrolling
+     */
+    private VelocityTracker mVelocityTracker;
+    private int mMinimumVelocity;
+    private int mMaximumVelocity;
+    private int mFlingDistance;
+    private int mCloseEnough;
+
+    // If the pager is at least this close to its final position, complete the scroll
+    // on touch down and let the user interact with the content inside instead of
+    // "catching" the flinging pager.
+    private static final int CLOSE_ENOUGH = 2; // dp
+
+    private boolean mFakeDragging;
+    private long mFakeDragBeginTime;
+
+    private EdgeEffectCompat mTopEdge;
+    private EdgeEffectCompat mBottomEdge;
+    private boolean mFirstLayout = true;
+    private boolean mNeedCalculatePageOffsets = false;
+    private boolean mCalledSuper;
+    private int mDecorChildCount;
+
+    private ViewPager.OnPageChangeListener mOnPageChangeListener;
+    private ViewPager.OnPageChangeListener mInternalPageChangeListener;
+    private OnAdapterChangeListener mAdapterChangeListener;
+    private ViewPager.PageTransformer mPageTransformer;
+    private Method mSetChildrenDrawingOrderEnabled;
+
+    private static final int DRAW_ORDER_DEFAULT = 0;
+    private static final int DRAW_ORDER_FORWARD = 1;
+    private static final int DRAW_ORDER_REVERSE = 2;
+    private int mDrawingOrder;
+    private ArrayList<View> mDrawingOrderedChildren;
+    private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();
+
+    /**
+     * Indicates that the pager is in an idle, settled state. The current page
+     * is fully in view and no animation is in progress.
+     */
+    public static final int SCROLL_STATE_IDLE = 0;
+
+    /**
+     * Indicates that the pager is currently being dragged by the user.
+     */
+    public static final int SCROLL_STATE_DRAGGING = 1;
+
+    /**
+     * Indicates that the pager is in the process of settling to a final position.
+     */
+    public static final int SCROLL_STATE_SETTLING = 2;
+
+    private final Runnable mEndScrollRunnable = new Runnable() {
+        public void run() {
+            setScrollState(SCROLL_STATE_IDLE);
+            populate();
+        }
+    };
+
+    private int mScrollState = SCROLL_STATE_IDLE;
+
+    // private ScrollerCustomDuration mScrollerCustomDuration = null;
+
+    /**
+     * Used internally to monitor when adapters are switched.
+     */
+    interface OnAdapterChangeListener {
+        public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter);
+    }
+
+    /**
+     * Used internally to tag special types of child views that should be added as
+     * pager decorations by default.
+     */
+    interface Decor {
+    }
+
+    public VerticalViewPager(Context context) {
+        super(context);
+        initViewPager();
+        //postInitViewPager();
+    }
+
+    public VerticalViewPager(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initViewPager();
+        // postInitViewPager();
+    }
+
+    void initViewPager() {
+        setWillNotDraw(false);
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+        setFocusable(true);
+        final Context context = getContext();
+        mScroller = new Scroller(context, sInterpolator);
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        final float density = context.getResources().getDisplayMetrics().density;
+
+        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
+        mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mTopEdge = new EdgeEffectCompat(context);
+        mBottomEdge = new EdgeEffectCompat(context);
+
+        mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
+        mCloseEnough = (int) (CLOSE_ENOUGH * density);
+        mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);
+
+        ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());
+
+        if (ViewCompat.getImportantForAccessibility(this)
+                == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            ViewCompat.setImportantForAccessibility(this,
+                    ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+        //postInitViewPager();
+    }
+
+
+   /* private void postInitViewPager() {
+        try {
+            Field scroller = ViewPager.class.getDeclaredField("mScroller");
+            scroller.setAccessible(true);
+            Field interpolator = ViewPager.class.getDeclaredField("sInterpolator");
+            interpolator.setAccessible(true);
+
+            mScrollerCustomDuration = new ScrollerCustomDuration(getContext(),
+                    (Interpolator) interpolator.get(null));
+            scroller.set(this, mScrollerCustomDuration);
+        } catch (Exception e) {
+        }
+    }
+
+    public void setScrollDurationFactor(double scrollFactor) {
+        mScrollerCustomDuration.setScrollDurationFactor(scrollFactor);
+    }*/
+
+    @Override
+    protected void onDetachedFromWindow() {
+        removeCallbacks(mEndScrollRunnable);
+        super.onDetachedFromWindow();
+    }
+
+    private void setScrollState(int newState) {
+        if (mScrollState == newState) {
+            return;
+        }
+
+        mScrollState = newState;
+        if (mPageTransformer != null) {
+            // PageTransformers can do complex things that benefit from hardware layers.
+            enableLayers(newState != SCROLL_STATE_IDLE);
+        }
+        if (mOnPageChangeListener != null) {
+            mOnPageChangeListener.onPageScrollStateChanged(newState);
+        }
+    }
+
+    /**
+     * Set a PagerAdapter that will supply views for this pager as needed.
+     *
+     * @param adapter Adapter to use
+     */
+    public void setAdapter(PagerAdapter adapter) {
+        if (mAdapter != null) {
+            mAdapter.unregisterDataSetObserver(mObserver);
+            mAdapter.startUpdate(this);
+            for (int i = 0; i < mItems.size(); i++) {
+                final ItemInfo ii = mItems.get(i);
+                mAdapter.destroyItem(this, ii.position, ii.object);
+            }
+            mAdapter.finishUpdate(this);
+            mItems.clear();
+            removeNonDecorViews();
+            mCurItem = 0;
+            scrollTo(0, 0);
+        }
+
+        final PagerAdapter oldAdapter = mAdapter;
+        mAdapter = adapter;
+        mExpectedAdapterCount = 0;
+
+        if (mAdapter != null) {
+            if (mObserver == null) {
+                mObserver = new PagerObserver();
+            }
+            mAdapter.registerDataSetObserver(mObserver);
+            mPopulatePending = false;
+            final boolean wasFirstLayout = mFirstLayout;
+            mFirstLayout = true;
+            mExpectedAdapterCount = mAdapter.getCount();
+            if (mRestoredCurItem >= 0) {
+                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
+                setCurrentItemInternal(mRestoredCurItem, false, true);
+                mRestoredCurItem = -1;
+                mRestoredAdapterState = null;
+                mRestoredClassLoader = null;
+            } else if (!wasFirstLayout) {
+                populate();
+            } else {
+                requestLayout();
+            }
+        }
+
+        if (mAdapterChangeListener != null && oldAdapter != adapter) {
+            mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
+        }
+    }
+
+    private void removeNonDecorViews() {
+        for (int i = 0; i < getChildCount(); i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (!lp.isDecor) {
+                removeViewAt(i);
+                i--;
+            }
+        }
+    }
+
+    /**
+     * Retrieve the current adapter supplying pages.
+     *
+     * @return The currently registered PagerAdapter
+     */
+    public PagerAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    void setOnAdapterChangeListener(OnAdapterChangeListener listener) {
+        mAdapterChangeListener = listener;
+    }
+
+//    private int getClientWidth() {
+//        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+//    }
+
+    private int getClientHeight() {
+        return getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+    }
+
+
+    /**
+     * Set the currently selected page. If the ViewPager has already been through its first
+     * layout with its current adapter there will be a smooth animated transition between
+     * the current item and the specified item.
+     *
+     * @param item Item index to select
+     */
+    public void setCurrentItem(int item) {
+        mPopulatePending = false;
+        setCurrentItemInternal(item, !mFirstLayout, false);
+    }
+
+    /**
+     * Set the currently selected page.
+     *
+     * @param item         Item index to select
+     * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
+     */
+    public void setCurrentItem(int item, boolean smoothScroll) {
+        mPopulatePending = false;
+        setCurrentItemInternal(item, smoothScroll, false);
+    }
+
+    public int getCurrentItem() {
+        return mCurItem;
+    }
+
+    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
+        setCurrentItemInternal(item, smoothScroll, always, 0);
+    }
+
+    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
+        if (mAdapter == null || mAdapter.getCount() <= 0) {
+            setScrollingCacheEnabled(false);
+            return;
+        }
+        if (!always && mCurItem == item && mItems.size() != 0) {
+            setScrollingCacheEnabled(false);
+            return;
+        }
+
+        if (item < 0) {
+            item = 0;
+        } else if (item >= mAdapter.getCount()) {
+            item = mAdapter.getCount() - 1;
+        }
+        final int pageLimit = mOffscreenPageLimit;
+        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
+            // We are doing a jump by more than one page.  To avoid
+            // glitches, we want to keep all current pages in the view
+            // until the scroll ends.
+            for (int i = 0; i < mItems.size(); i++) {
+                mItems.get(i).scrolling = true;
+            }
+        }
+        final boolean dispatchSelected = mCurItem != item;
+
+        if (mFirstLayout) {
+            // We don't have any idea how big we are yet and shouldn't have any pages either.
+            // Just set things up and let the pending layout handle things.
+            mCurItem = item;
+            if (dispatchSelected && mOnPageChangeListener != null) {
+                mOnPageChangeListener.onPageSelected(item);
+            }
+            if (dispatchSelected && mInternalPageChangeListener != null) {
+                mInternalPageChangeListener.onPageSelected(item);
+            }
+            requestLayout();
+        } else {
+            populate(item);
+            scrollToItem(item, smoothScroll, velocity, dispatchSelected);
+        }
+    }
+
+    private void scrollToItem(int item, boolean smoothScroll, int velocity,
+                              boolean dispatchSelected) {
+        final ItemInfo curInfo = infoForPosition(item);
+        int destY = 0;
+        if (curInfo != null) {
+            final int height = getClientHeight();
+            destY = (int) (height * Math.max(mFirstOffset,
+                    Math.min(curInfo.offset, mLastOffset)));
+        }
+        if (smoothScroll) {
+            smoothScrollTo(0, destY, velocity);
+            if (dispatchSelected && mOnPageChangeListener != null) {
+                mOnPageChangeListener.onPageSelected(item);
+            }
+            if (dispatchSelected && mInternalPageChangeListener != null) {
+                mInternalPageChangeListener.onPageSelected(item);
+            }
+        } else {
+            if (dispatchSelected && mOnPageChangeListener != null) {
+                mOnPageChangeListener.onPageSelected(item);
+            }
+            if (dispatchSelected && mInternalPageChangeListener != null) {
+                mInternalPageChangeListener.onPageSelected(item);
+            }
+            completeScroll(false);
+            scrollTo(0, destY);
+            pageScrolled(destY);
+        }
+    }
+
+    /**
+     * Set a listener that will be invoked whenever the page changes or is incrementally
+     * scrolled. See {@link ViewPager.OnPageChangeListener}.
+     *
+     * @param listener Listener to set
+     */
+    public void setOnPageChangeListener(OnPageChangeListener listener) {
+        mOnPageChangeListener = listener;
+    }
+
+    /**
+     * Set a {@link ViewPager.PageTransformer} that will be called for each attached page whenever
+     * the scroll position is changed. This allows the application to apply custom property
+     * transformations to each page, overriding the default sliding look and feel.
+     * <p/>
+     * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
+     * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no
+     * effect.</p>
+     *
+     * @param reverseDrawingOrder true if the supplied PageTransformer requires page views
+     *                            to be drawn from last to first instead of first to last.
+     * @param transformer         PageTransformer that will modify each page's animation properties
+     */
+    public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) {
+        if (Build.VERSION.SDK_INT >= 11) {
+            final boolean hasTransformer = transformer != null;
+            final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
+            mPageTransformer = transformer;
+            setChildrenDrawingOrderEnabledCompat(hasTransformer);
+            if (hasTransformer) {
+                mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
+            } else {
+                mDrawingOrder = DRAW_ORDER_DEFAULT;
+            }
+            if (needsPopulate) populate();
+        }
+    }
+
+    void setChildrenDrawingOrderEnabledCompat(boolean enable) {
+        if (Build.VERSION.SDK_INT >= 7) {
+            if (mSetChildrenDrawingOrderEnabled == null) {
+                try {
+                    mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod(
+                            "setChildrenDrawingOrderEnabled", new Class[]{Boolean.TYPE});
+                } catch (NoSuchMethodException e) {
+                    Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e);
+                }
+            }
+            try {
+                mSetChildrenDrawingOrderEnabled.invoke(this, enable);
+            } catch (Exception e) {
+                Log.e(TAG, "Error changing children drawing order", e);
+            }
+        }
+    }
+
+    @Override
+    protected int getChildDrawingOrder(int childCount, int i) {
+        final int index =
+                mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i;
+        final int result =
+                ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex;
+        return result;
+    }
+
+    /**
+     * Set a separate OnPageChangeListener for internal
+     * use by the support library.
+     *
+     * @param listener Listener to set
+     * @return The old listener that was set,
+     * if any.
+     */
+    OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) {
+        OnPageChangeListener oldListener = mInternalPageChangeListener;
+        mInternalPageChangeListener = listener;
+        return oldListener;
+    }
+
+    /**
+     * Returns the number of pages that will be retained to either side of the
+     * current page in the view hierarchy in an idle state. Defaults to 1.
+     *
+     * @return How many pages will be kept offscreen on either side
+     * @see #setOffscreenPageLimit(int)
+     */
+    public int getOffscreenPageLimit() {
+        return mOffscreenPageLimit;
+    }
+
+    /**
+     * Set the number of pages that should be retained to either side of the
+     * current page in the view hierarchy in an idle state. Pages beyond this
+     * limit will be recreated from the adapter when needed.
+     * <p/>
+     * <p>This is offered as an optimization. If you know in advance the number
+     * of pages you will need to support or have lazy-loading mechanisms in place
+     * on your pages, tweaking this setting can have benefits in perceived smoothness
+     * of paging animations and interaction. If you have a small number of pages (3-4)
+     * that you can keep active all at once, less time will be spent in layout for
+     * newly created view subtrees as the user pages back and forth.</p>
+     * <p/>
+     * <p>You should keep this limit low, especially if your pages have complex layouts.
+     * This setting defaults to 1.</p>
+     *
+     * @param limit How many pages will be kept offscreen in an idle state.
+     */
+    public void setOffscreenPageLimit(int limit) {
+        if (limit < DEFAULT_OFFSCREEN_PAGES) {
+            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
+                    DEFAULT_OFFSCREEN_PAGES);
+            limit = DEFAULT_OFFSCREEN_PAGES;
+        }
+        if (limit != mOffscreenPageLimit) {
+            mOffscreenPageLimit = limit;
+            populate();
+        }
+    }
+
+    /**
+     * Set the margin between pages.
+     *
+     * @param marginPixels Distance between adjacent pages in pixels
+     * @see #getPageMargin()
+     * @see #setPageMarginDrawable(Drawable)
+     * @see #setPageMarginDrawable(int)
+     */
+    public void setPageMargin(int marginPixels) {
+        final int oldMargin = mPageMargin;
+        mPageMargin = marginPixels;
+
+        final int height = getHeight();
+        recomputeScrollPosition(height, height, marginPixels, oldMargin);
+
+        requestLayout();
+    }
+
+    /**
+     * Return the margin between pages.
+     *
+     * @return The size of the margin in pixels
+     */
+    public int getPageMargin() {
+        return mPageMargin;
+    }
+
+    /**
+     * Set a drawable that will be used to fill the margin between pages.
+     *
+     * @param d Drawable to display between pages
+     */
+    public void setPageMarginDrawable(Drawable d) {
+        mMarginDrawable = d;
+        if (d != null) refreshDrawableState();
+        setWillNotDraw(d == null);
+        invalidate();
+    }
+
+    /**
+     * Set a drawable that will be used to fill the margin between pages.
+     *
+     * @param resId Resource ID of a drawable to display between pages
+     */
+    public void setPageMarginDrawable(int resId) {
+        setPageMarginDrawable(getContext().getResources().getDrawable(resId));
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return super.verifyDrawable(who) || who == mMarginDrawable;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        final Drawable d = mMarginDrawable;
+        if (d != null && d.isStateful()) {
+            d.setState(getDrawableState());
+        }
+    }
+
+    // We want the duration of the page snap animation to be influenced by the distance that
+    // the screen has to travel, however, we don't want this duration to be effected in a
+    // purely linear fashion. Instead, we use this method to moderate the effect that the distance
+    // of travel has on the overall snap duration.
+    float distanceInfluenceForSnapDuration(float f) {
+        f -= 0.5f; // center the values about 0.
+        f *= 0.3f * Math.PI / 2.0f;
+        return (float) Math.sin(f);
+    }
+
+    /**
+     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+     *
+     * @param x the number of pixels to scroll by on the X axis
+     * @param y the number of pixels to scroll by on the Y axis
+     */
+    void smoothScrollTo(int x, int y) {
+        smoothScrollTo(x, y, 0);
+    }
+
+    /**
+     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+     *
+     * @param x        the number of pixels to scroll by on the X axis
+     * @param y        the number of pixels to scroll by on the Y axis
+     * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
+     */
+    void smoothScrollTo(int x, int y, int velocity) {
+        if (getChildCount() == 0) {
+            // Nothing to do.
+            setScrollingCacheEnabled(false);
+            return;
+        }
+        int sx = getScrollX();
+        int sy = getScrollY();
+        int dx = x - sx;
+        int dy = y - sy;
+        if (dx == 0 && dy == 0) {
+            completeScroll(false);
+            populate();
+            setScrollState(SCROLL_STATE_IDLE);
+            return;
+        }
+
+        setScrollingCacheEnabled(true);
+        setScrollState(SCROLL_STATE_SETTLING);
+
+        final int height = getClientHeight();
+        final int halfHeight = height / 2;
+        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / height);
+        final float distance = halfHeight + halfHeight *
+                distanceInfluenceForSnapDuration(distanceRatio);
+
+        int duration = 0;
+        velocity = Math.abs(velocity);
+        if (velocity > 0) {
+            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+        } else {
+            final float pageHeight = height * mAdapter.getPageWidth(mCurItem);
+            final float pageDelta = (float) Math.abs(dx) / (pageHeight + mPageMargin);
+            duration = (int) ((pageDelta + 1) * 100);
+        }
+        duration = Math.min(duration, MAX_SETTLE_DURATION);
+
+        mScroller.startScroll(sx, sy, dx, dy, duration);
+        ViewCompat.postInvalidateOnAnimation(this);
+    }
+
+    ItemInfo addNewItem(int position, int index) {
+        ItemInfo ii = new ItemInfo();
+        ii.position = position;
+        ii.object = mAdapter.instantiateItem(this, position);
+        ii.heightFactor = mAdapter.getPageWidth(position);
+        if (index < 0 || index >= mItems.size()) {
+            mItems.add(ii);
+        } else {
+            mItems.add(index, ii);
+        }
+        return ii;
+    }
+
+    void dataSetChanged() {
+        // This method only gets called if our observer is attached, so mAdapter is non-null.
+
+        final int adapterCount = mAdapter.getCount();
+        mExpectedAdapterCount = adapterCount;
+        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
+                mItems.size() < adapterCount;
+        int newCurrItem = mCurItem;
+
+        boolean isUpdating = false;
+        for (int i = 0; i < mItems.size(); i++) {
+            final ItemInfo ii = mItems.get(i);
+            final int newPos = mAdapter.getItemPosition(ii.object);
+
+            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
+                continue;
+            }
+
+            if (newPos == PagerAdapter.POSITION_NONE) {
+                mItems.remove(i);
+                i--;
+
+                if (!isUpdating) {
+                    mAdapter.startUpdate(this);
+                    isUpdating = true;
+                }
+
+                mAdapter.destroyItem(this, ii.position, ii.object);
+                needPopulate = true;
+
+                if (mCurItem == ii.position) {
+                    // Keep the current item in the valid range
+                    newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
+                    needPopulate = true;
+                }
+                continue;
+            }
+
+            if (ii.position != newPos) {
+                if (ii.position == mCurItem) {
+                    // Our current item changed position. Follow it.
+                    newCurrItem = newPos;
+                }
+
+                ii.position = newPos;
+                needPopulate = true;
+            }
+        }
+
+        if (isUpdating) {
+            mAdapter.finishUpdate(this);
+        }
+
+        Collections.sort(mItems, COMPARATOR);
+
+        if (needPopulate) {
+            // Reset our known page widths; populate will recompute them.
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (!lp.isDecor) {
+                    lp.heightFactor = 0.f;
+                }
+            }
+
+            setCurrentItemInternal(newCurrItem, false, true);
+            requestLayout();
+        }
+    }
+
+    void populate() {
+        populate(mCurItem);
+    }
+
+    void populate(int newCurrentItem) {
+        ItemInfo oldCurInfo = null;
+        int focusDirection = View.FOCUS_FORWARD;
+        if (mCurItem != newCurrentItem) {
+            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_DOWN : View.FOCUS_UP;
+            oldCurInfo = infoForPosition(mCurItem);
+            mCurItem = newCurrentItem;
+        }
+
+        if (mAdapter == null) {
+            sortChildDrawingOrder();
+            return;
+        }
+
+        // Bail now if we are waiting to populate.  This is to hold off
+        // on creating views from the time the user releases their finger to
+        // fling to a new position until we have finished the scroll to
+        // that position, avoiding glitches from happening at that point.
+        if (mPopulatePending) {
+            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
+            sortChildDrawingOrder();
+            return;
+        }
+
+        // Also, don't populate until we are attached to a window.  This is to
+        // avoid trying to populate before we have restored our view hierarchy
+        // state and conflicting with what is restored.
+        if (getWindowToken() == null) {
+            return;
+        }
+
+        mAdapter.startUpdate(this);
+
+        final int pageLimit = mOffscreenPageLimit;
+        final int startPos = Math.max(0, mCurItem - pageLimit);
+        final int N = mAdapter.getCount();
+        final int endPos = Math.min(N - 1, mCurItem + pageLimit);
+
+        if (N != mExpectedAdapterCount) {
+            String resName;
+            try {
+                resName = getResources().getResourceName(getId());
+            } catch (Resources.NotFoundException e) {
+                resName = Integer.toHexString(getId());
+            }
+            throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
+                    " contents without calling PagerAdapter#notifyDataSetChanged!" +
+                    " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
+                    " Pager id: " + resName +
+                    " Pager class: " + getClass() +
+                    " Problematic adapter: " + mAdapter.getClass());
+        }
+
+        // Locate the currently focused item or add it if needed.
+        int curIndex = -1;
+        ItemInfo curItem = null;
+        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
+            final ItemInfo ii = mItems.get(curIndex);
+            if (ii.position >= mCurItem) {
+                if (ii.position == mCurItem) curItem = ii;
+                break;
+            }
+        }
+
+        if (curItem == null && N > 0) {
+            curItem = addNewItem(mCurItem, curIndex);
+        }
+
+        // Fill 3x the available width or up to the number of offscreen
+        // pages requested to either side, whichever is larger.
+        // If we have no current item we have no work to do.
+        if (curItem != null) {
+            float extraHeightTop = 0.f;
+            int itemIndex = curIndex - 1;
+            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+            final int clientHeight = getClientHeight();
+            final float topHeightNeeded = clientHeight <= 0 ? 0 :
+                    2.f - curItem.heightFactor + (float) getPaddingLeft() / (float) clientHeight;
+            for (int pos = mCurItem - 1; pos >= 0; pos--) {
+                if (extraHeightTop >= topHeightNeeded && pos < startPos) {
+                    if (ii == null) {
+                        break;
+                    }
+                    if (pos == ii.position && !ii.scrolling) {
+                        mItems.remove(itemIndex);
+                        mAdapter.destroyItem(this, pos, ii.object);
+                        if (DEBUG) {
+                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
+                                    " view: " + ((View) ii.object));
+                        }
+                        itemIndex--;
+                        curIndex--;
+                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+                    }
+                } else if (ii != null && pos == ii.position) {
+                    extraHeightTop += ii.heightFactor;
+                    itemIndex--;
+                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+                } else {
+                    ii = addNewItem(pos, itemIndex + 1);
+                    extraHeightTop += ii.heightFactor;
+                    curIndex++;
+                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+                }
+            }
+
+            float extraHeightBottom = curItem.heightFactor;
+            itemIndex = curIndex + 1;
+            if (extraHeightBottom < 2.f) {
+                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+                final float bottomHeightNeeded = clientHeight <= 0 ? 0 :
+                        (float) getPaddingRight() / (float) clientHeight + 2.f;
+                for (int pos = mCurItem + 1; pos < N; pos++) {
+                    if (extraHeightBottom >= bottomHeightNeeded && pos > endPos) {
+                        if (ii == null) {
+                            break;
+                        }
+                        if (pos == ii.position && !ii.scrolling) {
+                            mItems.remove(itemIndex);
+                            mAdapter.destroyItem(this, pos, ii.object);
+                            if (DEBUG) {
+                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
+                                        " view: " + ((View) ii.object));
+                            }
+                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+                        }
+                    } else if (ii != null && pos == ii.position) {
+                        extraHeightBottom += ii.heightFactor;
+                        itemIndex++;
+                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+                    } else {
+                        ii = addNewItem(pos, itemIndex);
+                        itemIndex++;
+                        extraHeightBottom += ii.heightFactor;
+                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+                    }
+                }
+            }
+
+            calculatePageOffsets(curItem, curIndex, oldCurInfo);
+        }
+
+        if (DEBUG) {
+            Log.i(TAG, "Current page list:");
+            for (int i = 0; i < mItems.size(); i++) {
+                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
+            }
+        }
+
+        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
+
+        mAdapter.finishUpdate(this);
+
+        // Check width measurement of current pages and drawing sort order.
+        // Update LayoutParams as needed.
+        final int childCount = getChildCount();
+
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            lp.childIndex = i;
+            if (!lp.isDecor && lp.heightFactor == 0.f) {
+                // 0 means requery the adapter for this, it doesn't have a valid width.
+                final ItemInfo ii = infoForChild(child);
+                if (ii != null) {
+                    lp.heightFactor = ii.heightFactor;
+                    lp.position = ii.position;
+                }
+            }
+        }
+        sortChildDrawingOrder();
+
+        if (hasFocus()) {
+            View currentFocused = findFocus();
+            ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
+            if (ii == null || ii.position != mCurItem) {
+                for (int i = 0; i < getChildCount(); i++) {
+                    View child = getChildAt(i);
+                    ii = infoForChild(child);
+                    if (ii != null && ii.position == mCurItem
+                            && child.requestFocus(focusDirection)) {
+//                        if (child.requestFocus(focusDirection)) {
+                        break;
+                        // }
+                    }
+                }
+            }
+        }
+    }
+
+    private void sortChildDrawingOrder() {
+        if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
+            if (mDrawingOrderedChildren == null) {
+                mDrawingOrderedChildren = new ArrayList<View>();
+            } else {
+                mDrawingOrderedChildren.clear();
+            }
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                mDrawingOrderedChildren.add(child);
+            }
+            Collections.sort(mDrawingOrderedChildren, sPositionComparator);
+        }
+    }
+
+    private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
+        final int N = mAdapter.getCount();
+        final int height = getClientHeight();
+        final float marginOffset = height > 0 ? (float) mPageMargin / height : 0;
+        // Fix up offsets for later layout.
+        if (oldCurInfo != null) {
+            final int oldCurPosition = oldCurInfo.position;
+            // Base offsets off of oldCurInfo.
+            if (oldCurPosition < curItem.position) {
+                int itemIndex = 0;
+                ItemInfo ii = null;
+                float offset = oldCurInfo.offset + oldCurInfo.heightFactor + marginOffset;
+                for (int pos = oldCurPosition + 1;
+                        pos <= curItem.position && itemIndex < mItems.size(); pos++) {
+                    ii = mItems.get(itemIndex);
+                    while (pos > ii.position && itemIndex < mItems.size() - 1) {
+                        itemIndex++;
+                        ii = mItems.get(itemIndex);
+                    }
+                    while (pos < ii.position) {
+                        // We don't have an item populated for this,
+                        // ask the adapter for an offset.
+                        offset += mAdapter.getPageWidth(pos) + marginOffset;
+                        pos++;
+                    }
+                    ii.offset = offset;
+                    offset += ii.heightFactor + marginOffset;
+                }
+            } else if (oldCurPosition > curItem.position) {
+                int itemIndex = mItems.size() - 1;
+                ItemInfo ii = null;
+                float offset = oldCurInfo.offset;
+                for (int pos = oldCurPosition - 1;
+                        pos >= curItem.position && itemIndex >= 0;
+                        pos--) {
+                    ii = mItems.get(itemIndex);
+                    while (pos < ii.position && itemIndex > 0) {
+                        itemIndex--;
+                        ii = mItems.get(itemIndex);
+                    }
+                    while (pos > ii.position) {
+                        // We don't have an item populated for this,
+                        // ask the adapter for an offset.
+                        offset -= mAdapter.getPageWidth(pos) + marginOffset;
+                        pos--;
+                    }
+                    offset -= ii.heightFactor + marginOffset;
+                    ii.offset = offset;
+                }
+            }
+        }
+
+        // Base all offsets off of curItem.
+        final int itemCount = mItems.size();
+        float offset = curItem.offset;
+        int pos = curItem.position - 1;
+        mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
+        mLastOffset = curItem.position == N - 1 ?
+                curItem.offset + curItem.heightFactor - 1 : Float.MAX_VALUE;
+        // Previous pages
+        for (int i = curIndex - 1; i >= 0; i--, pos--) {
+            final ItemInfo ii = mItems.get(i);
+            while (pos > ii.position) {
+                offset -= mAdapter.getPageWidth(pos--) + marginOffset;
+            }
+            offset -= ii.heightFactor + marginOffset;
+            ii.offset = offset;
+            if (ii.position == 0) mFirstOffset = offset;
+        }
+        offset = curItem.offset + curItem.heightFactor + marginOffset;
+        pos = curItem.position + 1;
+        // Next pages
+        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
+            final ItemInfo ii = mItems.get(i);
+            while (pos < ii.position) {
+                offset += mAdapter.getPageWidth(pos++) + marginOffset;
+            }
+            if (ii.position == N - 1) {
+                mLastOffset = offset + ii.heightFactor - 1;
+            }
+            ii.offset = offset;
+            offset += ii.heightFactor + marginOffset;
+        }
+
+        mNeedCalculatePageOffsets = false;
+    }
+
+    /**
+     * This is the persistent state that is saved by ViewPager.  Only needed
+     * if you are creating a sublass of ViewPager that must save its own
+     * state, in which case it should implement a subclass of this which
+     * contains that state.
+     */
+    public static class SavedState extends BaseSavedState {
+        int position;
+        Parcelable adapterState;
+        ClassLoader loader;
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(position);
+            out.writeParcelable(adapterState, flags);
+        }
+
+        @Override
+        public String toString() {
+            return "FragmentPager.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " position=" + position + "}";
+        }
+
+        public static final Creator<SavedState> CREATOR =
+                ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
+                    @Override
+                    public SavedState createFromParcel(Parcel in, ClassLoader loader) {
+                        return new SavedState(in, loader);
+                    }
+
+                    @Override
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                });
+
+        SavedState(Parcel in, ClassLoader loader) {
+            super(in);
+            if (loader == null) {
+                loader = getClass().getClassLoader();
+            }
+            position = in.readInt();
+            adapterState = in.readParcelable(loader);
+            this.loader = loader;
+        }
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState);
+        ss.position = mCurItem;
+        if (mAdapter != null) {
+            ss.adapterState = mAdapter.saveState();
+        }
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (!(state instanceof SavedState)) {
+            super.onRestoreInstanceState(state);
+            return;
+        }
+
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        if (mAdapter != null) {
+            mAdapter.restoreState(ss.adapterState, ss.loader);
+            setCurrentItemInternal(ss.position, false, true);
+        } else {
+            mRestoredCurItem = ss.position;
+            mRestoredAdapterState = ss.adapterState;
+            mRestoredClassLoader = ss.loader;
+        }
+    }
+
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (!checkLayoutParams(params)) {
+            params = generateLayoutParams(params);
+        }
+        final LayoutParams lp = (LayoutParams) params;
+        lp.isDecor |= child instanceof Decor;
+        if (mInLayout) {
+            if (lp != null && lp.isDecor) {
+                throw new IllegalStateException("Cannot add pager decor view during layout");
+            }
+            lp.needsMeasure = true;
+            addViewInLayout(child, index, params);
+        } else {
+            super.addView(child, index, params);
+        }
+
+        if (USE_CACHE) {
+            if (child.getVisibility() != GONE) {
+                child.setDrawingCacheEnabled(mScrollingCacheEnabled);
+            } else {
+                child.setDrawingCacheEnabled(false);
+            }
+        }
+    }
+
+    @Override
+    public void removeView(View view) {
+        if (mInLayout) {
+            removeViewInLayout(view);
+        } else {
+            super.removeView(view);
+        }
+    }
+
+    ItemInfo infoForChild(View child) {
+        for (int i = 0; i < mItems.size(); i++) {
+            ItemInfo ii = mItems.get(i);
+            if (mAdapter.isViewFromObject(child, ii.object)) {
+                return ii;
+            }
+        }
+        return null;
+    }
+
+    ItemInfo infoForAnyChild(View child) {
+        ViewParent parent;
+        while ((parent = child.getParent()) != this) {
+            if (parent == null || !(parent instanceof View)) {
+                return null;
+            }
+            child = (View) parent;
+        }
+        return infoForChild(child);
+    }
+
+    ItemInfo infoForPosition(int position) {
+        for (int i = 0; i < mItems.size(); i++) {
+            ItemInfo ii = mItems.get(i);
+            if (ii.position == position) {
+                return ii;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mFirstLayout = true;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // For simple implementation, our internal size is always 0.
+        // We depend on the container to specify the layout size of
+        // our view.  We can't really know what it is since we will be
+        // adding and removing different arbitrary views and do not
+        // want the layout to change as this happens.
+        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
+                getDefaultSize(0, heightMeasureSpec));
+
+        final int measuredHeight = getMeasuredHeight();
+        final int maxGutterSize = measuredHeight / 10;
+        mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
+
+        // Children are just made to fill our space.
+        int childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+        int childHeightSize = measuredHeight - getPaddingTop() - getPaddingBottom();
+
+        /*
+         * Make sure all children have been properly measured. Decor views first.
+         * Right now we cheat and make this less complicated by assuming decor
+         * views won't intersect. We will pin to edges based on gravity.
+         */
+        int size = getChildCount();
+        for (int i = 0; i < size; ++i) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp != null && lp.isDecor) {
+                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+                    int widthMode = MeasureSpec.AT_MOST;
+                    int heightMode = MeasureSpec.AT_MOST;
+                    boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
+                    boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
+
+                    if (consumeVertical) {
+                        widthMode = MeasureSpec.EXACTLY;
+                    } else if (consumeHorizontal) {
+                        heightMode = MeasureSpec.EXACTLY;
+                    }
+
+                    int widthSize = childWidthSize;
+                    int heightSize = childHeightSize;
+                    if (lp.width != LayoutParams.WRAP_CONTENT) {
+                        widthMode = MeasureSpec.EXACTLY;
+                        if (lp.width != LayoutParams.FILL_PARENT) {
+                            widthSize = lp.width;
+                        }
+                    }
+                    if (lp.height != LayoutParams.WRAP_CONTENT) {
+                        heightMode = MeasureSpec.EXACTLY;
+                        if (lp.height != LayoutParams.FILL_PARENT) {
+                            heightSize = lp.height;
+                        }
+                    }
+                    final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
+                    final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
+                    child.measure(widthSpec, heightSpec);
+
+                    if (consumeVertical) {
+                        childHeightSize -= child.getMeasuredHeight();
+                    } else if (consumeHorizontal) {
+                        childWidthSize -= child.getMeasuredWidth();
+                    }
+                }
+            }
+        }
+
+        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
+        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
+
+        // Make sure we have created all fragments that we need to have shown.
+        mInLayout = true;
+        populate();
+        mInLayout = false;
+
+        // Page views next.
+        size = getChildCount();
+        for (int i = 0; i < size; ++i) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
+                        + ": " + mChildWidthMeasureSpec);
+
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp == null || !lp.isDecor) {
+                    final int heightSpec = MeasureSpec.makeMeasureSpec(
+                            (int) (childHeightSize * lp.heightFactor), MeasureSpec.EXACTLY);
+                    child.measure(mChildWidthMeasureSpec, heightSpec);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        // Make sure scroll position is set correctly.
+        if (h != oldh) {
+            recomputeScrollPosition(h, oldh, mPageMargin, mPageMargin);
+        }
+    }
+
+    private void recomputeScrollPosition(int height, int oldHeight, int margin, int oldMargin) {
+        if (oldHeight > 0 && !mItems.isEmpty()) {
+            final int heightWithMargin = height - getPaddingTop() - getPaddingBottom() + margin;
+            final int oldHeightWithMargin = oldHeight - getPaddingTop() - getPaddingBottom()
+                    + oldMargin;
+            final int ypos = getScrollY();
+            final float pageOffset = (float) ypos / oldHeightWithMargin;
+            final int newOffsetPixels = (int) (pageOffset * heightWithMargin);
+
+            scrollTo(getScrollX(), newOffsetPixels);
+            if (!mScroller.isFinished()) {
+                // We now return to your regularly scheduled scroll, already in progress.
+                final int newDuration = mScroller.getDuration() - mScroller.timePassed();
+                ItemInfo targetInfo = infoForPosition(mCurItem);
+                mScroller.startScroll(0, newOffsetPixels,
+                        0, (int) (targetInfo.offset * height), newDuration);
+            }
+        } else {
+            final ItemInfo ii = infoForPosition(mCurItem);
+            final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
+            final int scrollPos = (int) (scrollOffset *
+                    (height - getPaddingTop() - getPaddingBottom()));
+            if (scrollPos != getScrollY()) {
+                completeScroll(false);
+                scrollTo(getScrollX(), scrollPos);
+            }
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int count = getChildCount();
+        int width = r - l;
+        int height = b - t;
+        int paddingLeft = getPaddingLeft();
+        int paddingTop = getPaddingTop();
+        int paddingRight = getPaddingRight();
+        int paddingBottom = getPaddingBottom();
+        final int scrollY = getScrollY();
+
+        int decorCount = 0;
+
+        // First pass - decor views. We need to do this in two passes so that
+        // we have the proper offsets for non-decor views later.
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                int childLeft = 0;
+                int childTop = 0;
+                if (lp.isDecor) {
+                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+                    switch (hgrav) {
+                        default:
+                            childLeft = paddingLeft;
+                            break;
+                        case Gravity.LEFT:
+                            childLeft = paddingLeft;
+                            paddingLeft += child.getMeasuredWidth();
+                            break;
+                        case Gravity.CENTER_HORIZONTAL:
+                            childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
+                                    paddingLeft);
+                            break;
+                        case Gravity.RIGHT:
+                            childLeft = width - paddingRight - child.getMeasuredWidth();
+                            paddingRight += child.getMeasuredWidth();
+                            break;
+                    }
+                    switch (vgrav) {
+                        default:
+                            childTop = paddingTop;
+                            break;
+                        case Gravity.TOP:
+                            childTop = paddingTop;
+                            paddingTop += child.getMeasuredHeight();
+                            break;
+                        case Gravity.CENTER_VERTICAL:
+                            childTop = Math.max((height - child.getMeasuredHeight()) / 2,
+                                    paddingTop);
+                            break;
+                        case Gravity.BOTTOM:
+                            childTop = height - paddingBottom - child.getMeasuredHeight();
+                            paddingBottom += child.getMeasuredHeight();
+                            break;
+                    }
+                    childTop += scrollY;
+                    child.layout(childLeft, childTop,
+                            childLeft + child.getMeasuredWidth(),
+                            childTop + child.getMeasuredHeight());
+                    decorCount++;
+                }
+            }
+        }
+
+        final int childHeight = height - paddingTop - paddingBottom;
+        // Page views. Do this once we have the right padding offsets from above.
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                ItemInfo ii;
+                if (!lp.isDecor && (ii = infoForChild(child)) != null) {
+                    int toff = (int) (childHeight * ii.offset);
+                    int childLeft = paddingLeft;
+                    int childTop = paddingTop + toff;
+                    if (lp.needsMeasure) {
+                        // This was added during layout and needs measurement.
+                        // Do it now that we know what we're working with.
+                        lp.needsMeasure = false;
+                        final int widthSpec = MeasureSpec.makeMeasureSpec(
+                                (int) (width - paddingLeft - paddingRight),
+                                MeasureSpec.EXACTLY);
+                        final int heightSpec = MeasureSpec.makeMeasureSpec(
+                                (int) (childHeight * lp.heightFactor),
+                                MeasureSpec.EXACTLY);
+                        child.measure(widthSpec, heightSpec);
+                    }
+                    if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
+                            + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
+                            + "x" + child.getMeasuredHeight());
+                    child.layout(childLeft, childTop,
+                            childLeft + child.getMeasuredWidth(),
+                            childTop + child.getMeasuredHeight());
+                }
+            }
+        }
+        mLeftPageBounds = paddingLeft;
+        mRightPageBounds = width - paddingRight;
+        mDecorChildCount = decorCount;
+
+        if (mFirstLayout) {
+            scrollToItem(mCurItem, false, 0, false);
+        }
+        mFirstLayout = false;
+    }
+
+    @Override
+    public void computeScroll() {
+        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
+            int oldX = getScrollX();
+            int oldY = getScrollY();
+            int x = mScroller.getCurrX();
+            int y = mScroller.getCurrY();
+
+            if (oldX != x || oldY != y) {
+                scrollTo(x, y);
+                if (!pageScrolled(y)) {
+                    mScroller.abortAnimation();
+                    scrollTo(x, 0);
+                }
+            }
+
+            // Keep on drawing until the animation has finished.
+            ViewCompat.postInvalidateOnAnimation(this);
+            return;
+        }
+
+        // Done with scroll, clean up state.
+        completeScroll(true);
+    }
+
+    private boolean pageScrolled(int ypos) {
+        if (mItems.size() == 0) {
+            mCalledSuper = false;
+            onPageScrolled(0, 0, 0);
+            if (!mCalledSuper) {
+                throw new IllegalStateException(
+                        "onPageScrolled did not call superclass implementation");
+            }
+            return false;
+        }
+        final ItemInfo ii = infoForCurrentScrollPosition();
+        final int height = getClientHeight();
+        final int heightWithMargin = height + mPageMargin;
+        final float marginOffset = (float) mPageMargin / height;
+        final int currentPage = ii.position;
+        final float pageOffset = (((float) ypos / height) - ii.offset) /
+                (ii.heightFactor + marginOffset);
+        final int offsetPixels = (int) (pageOffset * heightWithMargin);
+
+        mCalledSuper = false;
+        onPageScrolled(currentPage, pageOffset, offsetPixels);
+        if (!mCalledSuper) {
+            throw new IllegalStateException(
+                    "onPageScrolled did not call superclass implementation");
+        }
+        return true;
+    }
+
+    /**
+     * This method will be invoked when the current page is scrolled, either as part
+     * of a programmatically initiated smooth scroll or a user initiated touch scroll.
+     * If you override this method you must call through to the superclass implementation
+     * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
+     * returns.
+     *
+     * @param position     Position index of the first page currently being displayed.
+     *                     Page position+1 will be visible if positionOffset is nonzero.
+     * @param offset       Value from [0, 1) indicating the offset from the page at position.
+     * @param offsetPixels Value in pixels indicating the offset from position.
+     */
+    protected void onPageScrolled(int position, float offset, int offsetPixels) {
+        // Offset any decor views if needed - keep them on-screen at all times.
+        if (mDecorChildCount > 0) {
+            final int scrollY = getScrollY();
+            int paddingTop = getPaddingTop();
+            int paddingBottom = getPaddingBottom();
+            final int height = getHeight();
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (!lp.isDecor) continue;
+
+                final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+                int childTop = 0;
+                switch (vgrav) {
+                    default:
+                        childTop = paddingTop;
+                        break;
+                    case Gravity.TOP:
+                        childTop = paddingTop;
+                        paddingTop += child.getHeight();
+                        break;
+                    case Gravity.CENTER_VERTICAL:
+                        childTop = Math.max((height - child.getMeasuredHeight()) / 2,
+                                paddingTop);
+                        break;
+                    case Gravity.BOTTOM:
+                        childTop = height - paddingBottom - child.getMeasuredHeight();
+                        paddingBottom += child.getMeasuredHeight();
+                        break;
+                }
+                childTop += scrollY;
+
+                final int childOffset = childTop - child.getTop();
+                if (childOffset != 0) {
+                    child.offsetTopAndBottom(childOffset);
+                }
+            }
+        }
+
+        if (mOnPageChangeListener != null) {
+            mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+        }
+        if (mInternalPageChangeListener != null) {
+            mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+        }
+
+        if (mPageTransformer != null) {
+            final int scrollY = getScrollY();
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+                if (lp.isDecor) continue;
+
+                final float transformPos = (float) (child.getTop() - scrollY) / getClientHeight();
+                mPageTransformer.transformPage(child, transformPos);
+            }
+        }
+
+        mCalledSuper = true;
+    }
+
+    private void completeScroll(boolean postEvents) {
+        boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
+        if (needPopulate) {
+            // Done with scroll, no longer want to cache view drawing.
+            setScrollingCacheEnabled(false);
+            mScroller.abortAnimation();
+            int oldX = getScrollX();
+            int oldY = getScrollY();
+            int x = mScroller.getCurrX();
+            int y = mScroller.getCurrY();
+            if (oldX != x || oldY != y) {
+                scrollTo(x, y);
+            }
+        }
+        mPopulatePending = false;
+        for (int i = 0; i < mItems.size(); i++) {
+            ItemInfo ii = mItems.get(i);
+            if (ii.scrolling) {
+                needPopulate = true;
+                ii.scrolling = false;
+            }
+        }
+        if (needPopulate) {
+            if (postEvents) {
+                ViewCompat.postOnAnimation(this, mEndScrollRunnable);
+            } else {
+                mEndScrollRunnable.run();
+            }
+        }
+    }
+
+    private boolean isGutterDrag(float y, float dy) {
+        return (y < mGutterSize && dy > 0) || (y > getHeight() - mGutterSize && dy < 0);
+    }
+
+    private void enableLayers(boolean enable) {
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final int layerType = enable ?
+                    ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE;
+            ViewCompat.setLayerType(getChildAt(i), layerType, null);
+        }
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        /*
+         * This method JUST determines whether we want to intercept the motion.
+         * If we return true, onMotionEvent will be called and we do the actual
+         * scrolling there.
+         */
+
+        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+
+        // Always take care of the touch gesture being complete.
+        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+            // Release the drag.
+            if (DEBUG) Log.v(TAG, "Intercept done!");
+            mIsBeingDragged = false;
+            mIsUnableToDrag = false;
+            mActivePointerId = INVALID_POINTER;
+            if (mVelocityTracker != null) {
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+            }
+            return false;
+        }
+
+        // Nothing more to do here if we have decided whether or not we
+        // are dragging.
+        if (action != MotionEvent.ACTION_DOWN) {
+            if (mIsBeingDragged) {
+                if (DEBUG) Log.v(TAG, "Intercept returning true!");
+                return true;
+            }
+            if (mIsUnableToDrag) {
+                if (DEBUG) Log.v(TAG, "Intercept returning false!");
+                return false;
+            }
+        }
+
+        switch (action) {
+            case MotionEvent.ACTION_MOVE: {
+                /*
+                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+                 * whether the user has moved far enough from his original down touch.
+                 */
+
+                /*
+                * Locally do absolute value. mLastMotionY is set to the y value
+                * of the down event.
+                */
+                final int activePointerId = mActivePointerId;
+                if (activePointerId == INVALID_POINTER) {
+                    // If we don't have a valid id, the touch down wasn't on content.
+                    break;
+                }
+
+                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
+                final float y = MotionEventCompat.getY(ev, pointerIndex);
+                final float dy = y - mLastMotionY;
+                final float yDiff = Math.abs(dy);
+                final float x = MotionEventCompat.getX(ev, pointerIndex);
+                final float xDiff = Math.abs(x - mInitialMotionX);
+                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+
+                if (dy != 0 && !isGutterDrag(mLastMotionY, dy) &&
+                        canScroll(this, false, (int) dy, (int) x, (int) y)) {
+                    // Nested view has scrollable area under this point. Let it be handled there.
+                    mLastMotionX = x;
+                    mLastMotionY = y;
+                    mIsUnableToDrag = true;
+                    return false;
+                }
+                if (yDiff > mTouchSlop && yDiff * 0.5f > xDiff) {
+                    if (DEBUG) Log.v(TAG, "Starting drag!");
+                    mIsBeingDragged = true;
+                    requestParentDisallowInterceptTouchEvent(true);
+                    setScrollState(SCROLL_STATE_DRAGGING);
+                    mLastMotionY = dy > 0 ? mInitialMotionY + mTouchSlop :
+                            mInitialMotionY - mTouchSlop;
+                    mLastMotionX = x;
+                    setScrollingCacheEnabled(true);
+                } else if (xDiff > mTouchSlop) {
+                    // The finger has moved enough in the vertical
+                    // direction to be counted as a drag...  abort
+                    // any attempt to drag horizontally, to work correctly
+                    // with children that have scrolling containers.
+                    if (DEBUG) Log.v(TAG, "Starting unable to drag!");
+                    mIsUnableToDrag = true;
+                }
+                if (mIsBeingDragged && performDrag(y)) {
+                    // Scroll to follow the motion event
+                    ViewCompat.postInvalidateOnAnimation(this);
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_DOWN: {
+                /*
+                 * Remember location of down touch.
+                 * ACTION_DOWN always refers to pointer index 0.
+                 */
+                mLastMotionX = mInitialMotionX = ev.getX();
+                mLastMotionY = mInitialMotionY = ev.getY();
+                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+                mIsUnableToDrag = false;
+
+                mScroller.computeScrollOffset();
+                if (mScrollState == SCROLL_STATE_SETTLING &&
+                        Math.abs(mScroller.getFinalY() - mScroller.getCurrY()) > mCloseEnough) {
+                    // Let the user 'catch' the pager as it animates.
+                    mScroller.abortAnimation();
+                    mPopulatePending = false;
+                    populate();
+                    mIsBeingDragged = true;
+                    requestParentDisallowInterceptTouchEvent(true);
+                    setScrollState(SCROLL_STATE_DRAGGING);
+                } else {
+                    completeScroll(false);
+                    mIsBeingDragged = false;
+                }
+
+                if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
+                        + " mIsBeingDragged=" + mIsBeingDragged
+                        + "mIsUnableToDrag=" + mIsUnableToDrag);
+                break;
+            }
+
+            case MotionEventCompat.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                break;
+        }
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+
+        /*
+         * The only time we want to intercept motion events is if we are in the
+         * drag mode.
+         */
+        return mIsBeingDragged;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mFakeDragging) {
+            // A fake drag is in progress already, ignore this real one
+            // but still eat the touch events.
+            // (It is likely that the user is multi-touching the screen.)
+            return true;
+        }
+
+        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
+            // Don't handle edge touches immediately -- they may actually belong to one of our
+            // descendants.
+            return false;
+        }
+
+        if (mAdapter == null || mAdapter.getCount() == 0) {
+            // Nothing to present or scroll; nothing to touch.
+            return false;
+        }
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+
+        final int action = ev.getAction();
+        boolean needsInvalidate = false;
+
+        switch (action & MotionEventCompat.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                mScroller.abortAnimation();
+                mPopulatePending = false;
+                populate();
+
+                // Remember where the motion event started
+                mLastMotionX = mInitialMotionX = ev.getX();
+                mLastMotionY = mInitialMotionY = ev.getY();
+                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE:
+                if (!mIsBeingDragged) {
+                    final int pointerIndex =
+                            MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+                    final float y = MotionEventCompat.getY(ev, pointerIndex);
+                    final float yDiff = Math.abs(y - mLastMotionY);
+                    final float x = MotionEventCompat.getX(ev, pointerIndex);
+                    final float xDiff = Math.abs(x - mLastMotionX);
+                    if (DEBUG)
+                        Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+                    if (yDiff > mTouchSlop && yDiff > xDiff) {
+                        if (DEBUG) Log.v(TAG, "Starting drag!");
+                        mIsBeingDragged = true;
+                        requestParentDisallowInterceptTouchEvent(true);
+                        mLastMotionY = y - mInitialMotionY > 0 ? mInitialMotionY + mTouchSlop :
+                                mInitialMotionY - mTouchSlop;
+                        mLastMotionX = x;
+                        setScrollState(SCROLL_STATE_DRAGGING);
+                        setScrollingCacheEnabled(true);
+
+                        // Disallow Parent Intercept, just in case
+                        ViewParent parent = getParent();
+                        if (parent != null) {
+                            parent.requestDisallowInterceptTouchEvent(true);
+                        }
+                    }
+                }
+                // Not else! Note that mIsBeingDragged can be set above.
+                if (mIsBeingDragged) {
+                    // Scroll to follow the motion event
+                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
+                            ev, mActivePointerId);
+                    final float y = MotionEventCompat.getY(ev, activePointerIndex);
+                    needsInvalidate |= performDrag(y);
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mIsBeingDragged) {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                    int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
+                            velocityTracker, mActivePointerId);
+                    mPopulatePending = true;
+                    final int height = getClientHeight();
+                    final int scrollY = getScrollY();
+                    final ItemInfo ii = infoForCurrentScrollPosition();
+                    final int currentPage = ii.position;
+                    final float pageOffset =
+                            (((float) scrollY / height) - ii.offset) / ii.heightFactor;
+                    final int activePointerIndex =
+                            MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+                    final float y = MotionEventCompat.getY(ev, activePointerIndex);
+                    final int totalDelta = (int) (y - mInitialMotionY);
+                    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
+                            totalDelta);
+                    setCurrentItemInternal(nextPage, true, true, initialVelocity);
+
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                    needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsBeingDragged) {
+                    scrollToItem(mCurItem, true, 0, false);
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                    needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();
+                }
+                break;
+            case MotionEventCompat.ACTION_POINTER_DOWN: {
+                final int index = MotionEventCompat.getActionIndex(ev);
+                final float y = MotionEventCompat.getY(ev, index);
+                mLastMotionY = y;
+                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
+                break;
+            }
+            case MotionEventCompat.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                mLastMotionY = MotionEventCompat.getY(ev,
+                        MotionEventCompat.findPointerIndex(ev, mActivePointerId));
+                break;
+        }
+        if (needsInvalidate) {
+            ViewCompat.postInvalidateOnAnimation(this);
+        }
+        return true;
+    }
+
+    private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        final ViewParent parent = getParent();
+        if (parent != null) {
+            parent.requestDisallowInterceptTouchEvent(disallowIntercept);
+        }
+    }
+
+    private boolean performDrag(float y) {
+        boolean needsInvalidate = false;
+
+        final float deltaY = mLastMotionY - y;
+        mLastMotionY = y;
+
+        float oldScrollY = getScrollY();
+        float scrollY = oldScrollY + deltaY;
+        final int height = getClientHeight();
+
+        float topBound = height * mFirstOffset;
+        float bottomBound = height * mLastOffset;
+        boolean topAbsolute = true;
+        boolean bottomAbsolute = true;
+
+        final ItemInfo firstItem = mItems.get(0);
+        final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+        if (firstItem.position != 0) {
+            topAbsolute = false;
+            topBound = firstItem.offset * height;
+        }
+        if (lastItem.position != mAdapter.getCount() - 1) {
+            bottomAbsolute = false;
+            bottomBound = lastItem.offset * height;
+        }
+
+        if (scrollY < topBound) {
+            if (topAbsolute) {
+                float over = topBound - scrollY;
+                needsInvalidate = mTopEdge.onPull(Math.abs(over) / height);
+            }
+            scrollY = topBound;
+        } else if (scrollY > bottomBound) {
+            if (bottomAbsolute) {
+                float over = scrollY - bottomBound;
+                needsInvalidate = mBottomEdge.onPull(Math.abs(over) / height);
+            }
+            scrollY = bottomBound;
+        }
+        // Don't lose the rounded component
+        mLastMotionX += scrollY - (int) scrollY;
+        scrollTo(getScrollX(), (int) scrollY);
+        pageScrolled((int) scrollY);
+
+        return needsInvalidate;
+    }
+
+    /**
+     * @return Info about the page at the current scroll position.
+     * This can be synthetic for a missing middle page; the 'object' field can be null.
+     */
+    private ItemInfo infoForCurrentScrollPosition() {
+        final int height = getClientHeight();
+        final float scrollOffset = height > 0 ? (float) getScrollY() / height : 0;
+        final float marginOffset = height > 0 ? (float) mPageMargin / height : 0;
+        int lastPos = -1;
+        float lastOffset = 0.f;
+        float lastHeight = 0.f;
+        boolean first = true;
+
+        ItemInfo lastItem = null;
+        for (int i = 0; i < mItems.size(); i++) {
+            ItemInfo ii = mItems.get(i);
+            float offset;
+            if (!first && ii.position != lastPos + 1) {
+                // Create a synthetic item for a missing page.
+                ii = mTempItem;
+                ii.offset = lastOffset + lastHeight + marginOffset;
+                ii.position = lastPos + 1;
+                ii.heightFactor = mAdapter.getPageWidth(ii.position);
+                i--;
+            }
+            offset = ii.offset;
+
+            final float topBound = offset;
+            final float bottomBound = offset + ii.heightFactor + marginOffset;
+            if (first || scrollOffset >= topBound) {
+                if (scrollOffset < bottomBound || i == mItems.size() - 1) {
+                    return ii;
+                }
+            } else {
+                return lastItem;
+            }
+            first = false;
+            lastPos = ii.position;
+            lastOffset = offset;
+            lastHeight = ii.heightFactor;
+            lastItem = ii;
+        }
+
+        return lastItem;
+    }
+
+    private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaY) {
+        int targetPage;
+        if (Math.abs(deltaY) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
+            targetPage = velocity > 0 ? currentPage : currentPage + 1;
+        } else {
+            final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
+            targetPage = (int) (currentPage + pageOffset + truncator);
+        }
+
+        if (mItems.size() > 0) {
+            final ItemInfo firstItem = mItems.get(0);
+            final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+
+            // Only let the user target pages we have items for
+            targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
+        }
+
+        return targetPage;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+        boolean needsInvalidate = false;
+
+        final int overScrollMode = ViewCompat.getOverScrollMode(this);
+        if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+                (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
+                        mAdapter != null && mAdapter.getCount() > 1)) {
+            if (!mTopEdge.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int height = getHeight();
+                final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+
+                canvas.translate(getPaddingLeft(), mFirstOffset * height);
+                mTopEdge.setSize(width, height);
+                needsInvalidate |= mTopEdge.draw(canvas);
+                canvas.restoreToCount(restoreCount);
+            }
+            if (!mBottomEdge.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int height = getHeight();
+                final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+
+                canvas.rotate(180);
+                canvas.translate(-width - getPaddingLeft(), -(mLastOffset + 1) * height);
+                mBottomEdge.setSize(width, height);
+                needsInvalidate |= mBottomEdge.draw(canvas);
+                canvas.restoreToCount(restoreCount);
+            }
+        } else {
+            mTopEdge.finish();
+            mBottomEdge.finish();
+        }
+
+        if (needsInvalidate) {
+            // Keep animating
+            ViewCompat.postInvalidateOnAnimation(this);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // Draw the margin drawable between pages if needed.
+        if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
+            final int scrollY = getScrollY();
+            final int height = getHeight();
+
+            final float marginOffset = (float) mPageMargin / height;
+            int itemIndex = 0;
+            ItemInfo ii = mItems.get(0);
+            float offset = ii.offset;
+            final int itemCount = mItems.size();
+            final int firstPos = ii.position;
+            final int lastPos = mItems.get(itemCount - 1).position;
+            for (int pos = firstPos; pos < lastPos; pos++) {
+                while (pos > ii.position && itemIndex < itemCount) {
+                    ii = mItems.get(++itemIndex);
+                }
+
+                float drawAt;
+                if (pos == ii.position) {
+                    drawAt = (ii.offset + ii.heightFactor) * height;
+                    offset = ii.offset + ii.heightFactor + marginOffset;
+                } else {
+                    float heightFactor = mAdapter.getPageWidth(pos);
+                    drawAt = (offset + heightFactor) * height;
+                    offset += heightFactor + marginOffset;
+                }
+
+                if (drawAt + mPageMargin > scrollY) {
+                    mMarginDrawable.setBounds(mLeftPageBounds, (int) drawAt,
+                            mRightPageBounds, (int) (drawAt + mPageMargin + 0.5f));
+                    mMarginDrawable.draw(canvas);
+                }
+
+                if (drawAt > scrollY + height) {
+                    break; // No more visible, no sense in continuing
+                }
+            }
+        }
+    }
+
+    /**
+     * Start a fake drag of the pager.
+     * <p>
+     * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager
+     * with the touch scrolling of another view, while still letting the ViewPager
+     * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.)
+     * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call
+     * {@link #endFakeDrag()} to complete the fake drag and fling as necessary.
+     * <p>
+     * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag
+     * is already in progress, this method will return false.
+     *
+     * @return true if the fake drag began successfully, false if it could not be started.
+     * @see #fakeDragBy(float)
+     * @see #endFakeDrag()
+     */
+    public boolean beginFakeDrag() {
+        if (mIsBeingDragged) {
+            return false;
+        }
+        mFakeDragging = true;
+        setScrollState(SCROLL_STATE_DRAGGING);
+        mInitialMotionY = mLastMotionY = 0;
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+        final long time = SystemClock.uptimeMillis();
+        final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
+        mVelocityTracker.addMovement(ev);
+        ev.recycle();
+        mFakeDragBeginTime = time;
+        return true;
+    }
+
+    /**
+     * End a fake drag of the pager.
+     *
+     * @see #beginFakeDrag()
+     * @see #fakeDragBy(float)
+     */
+    public void endFakeDrag() {
+        if (!mFakeDragging) {
+            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
+        }
+
+        final VelocityTracker velocityTracker = mVelocityTracker;
+        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+        int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
+                velocityTracker, mActivePointerId);
+        mPopulatePending = true;
+        final int height = getClientHeight();
+        final int scrollY = getScrollY();
+        final ItemInfo ii = infoForCurrentScrollPosition();
+        final int currentPage = ii.position;
+        final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor;
+        final int totalDelta = (int) (mLastMotionY - mInitialMotionY);
+        int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
+                totalDelta);
+        setCurrentItemInternal(nextPage, true, true, initialVelocity);
+        endDrag();
+
+        mFakeDragging = false;
+    }
+
+    /**
+     * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
+     *
+     * @param yOffset Offset in pixels to drag by.
+     * @see #beginFakeDrag()
+     * @see #endFakeDrag()
+     */
+    public void fakeDragBy(float yOffset) {
+        if (!mFakeDragging) {
+            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
+        }
+
+
+        mLastMotionY += yOffset;
+
+        float oldScrollY = getScrollY();
+        float scrollY = oldScrollY - yOffset;
+        final int height = getClientHeight();
+
+        float topBound = height * mFirstOffset;
+        float bottomBound = height * mLastOffset;
+
+        final ItemInfo firstItem = mItems.get(0);
+        final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+        if (firstItem.position != 0) {
+            topBound = firstItem.offset * height;
+        }
+        if (lastItem.position != mAdapter.getCount() - 1) {
+            bottomBound = lastItem.offset * height;
+        }
+
+        if (scrollY < topBound) {
+            scrollY = topBound;
+        } else if (scrollY > bottomBound) {
+            scrollY = bottomBound;
+        }
+        // Don't lose the rounded component
+        mLastMotionY += scrollY - (int) scrollY;
+        scrollTo(getScrollX(), (int) scrollY);
+        pageScrolled((int) scrollY);
+
+        // Synthesize an event for the VelocityTracker.
+        final long time = SystemClock.uptimeMillis();
+        final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
+                0, mLastMotionY, 0);
+        mVelocityTracker.addMovement(ev);
+        ev.recycle();
+    }
+
+    /**
+     * Returns true if a fake drag is in progress.
+     *
+     * @return true if currently in a fake drag, false otherwise.
+     * @see #beginFakeDrag()
+     * @see #fakeDragBy(float)
+     * @see #endFakeDrag()
+     */
+    public boolean isFakeDragging() {
+        return mFakeDragging;
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
+        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
+            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
+            if (mVelocityTracker != null) {
+                mVelocityTracker.clear();
+            }
+        }
+    }
+
+    private void endDrag() {
+        mIsBeingDragged = false;
+        mIsUnableToDrag = false;
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private void setScrollingCacheEnabled(boolean enabled) {
+        if (mScrollingCacheEnabled != enabled) {
+            mScrollingCacheEnabled = enabled;
+            if (USE_CACHE) {
+                final int size = getChildCount();
+                for (int i = 0; i < size; ++i) {
+                    final View child = getChildAt(i);
+                    if (child.getVisibility() != GONE) {
+                        child.setDrawingCacheEnabled(enabled);
+                    }
+                }
+            }
+        }
+    }
+
+    public boolean internalCanScrollVertically(int direction) {
+        if (mAdapter == null) {
+            return false;
+        }
+
+        final int height = getClientHeight();
+        final int scrollY = getScrollY();
+        if (direction < 0) {
+            return (scrollY > (int) (height * mFirstOffset));
+        } else if (direction > 0) {
+            return (scrollY < (int) (height * mLastOffset));
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Tests scrollability within child views of v given a delta of dx.
+     *
+     * @param v      View to test for horizontal scrollability
+     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
+     *               or just its children (false).
+     * @param dy     Delta scrolled in pixels
+     * @param x      X coordinate of the active touch point
+     * @param y      Y coordinate of the active touch point
+     * @return true if child views of v can be scrolled by delta of dx.
+     */
+    protected boolean canScroll(View v, boolean checkV, int dy, int x, int y) {
+        if (v instanceof ViewGroup) {
+            final ViewGroup group = (ViewGroup) v;
+            final int scrollX = v.getScrollX();
+            final int scrollY = v.getScrollY();
+            final int count = group.getChildCount();
+            // Count backwards - let topmost views consume scroll distance first.
+            for (int i = count - 1; i >= 0; i--) {
+                // TODO: Add versioned support here for transformed views.
+                // This will not work for transformed views in Honeycomb+
+                final View child = group.getChildAt(i);
+                if (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
+                        x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
+                        canScroll(child, true, dy, x + scrollX - child.getLeft(),
+                                y + scrollY - child.getTop())) {
+                    return true;
+                }
+            }
+
+        }
+
+        return checkV && ViewCompat.canScrollVertically(v, -dy);
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        // Let the focused view and/or our descendants get the key first
+        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
+    }
+
+    /**
+     * You can call this function yourself to have the scroll view perform
+     * scrolling from a key event, just as if the event had been dispatched to
+     * it by the view hierarchy.
+     *
+     * @param event The key event to execute.
+     * @return Return true if the event was handled, else false.
+     */
+    public boolean executeKeyEvent(KeyEvent event) {
+        boolean handled = false;
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_DPAD_LEFT:
+                    handled = arrowScroll(FOCUS_LEFT);
+                    break;
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                    handled = arrowScroll(FOCUS_RIGHT);
+                    break;
+                case KeyEvent.KEYCODE_TAB:
+                    if (Build.VERSION.SDK_INT >= 11) {
+                        // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
+                        // before Android 3.0. Ignore the tab key on those devices.
+                        if (KeyEvent.metaStateHasNoModifiers(event.getMetaState())) {
+                            handled = arrowScroll(FOCUS_FORWARD);
+                        } else if (KeyEvent.metaStateHasNoModifiers(event.getMetaState())) {
+                            handled = arrowScroll(FOCUS_BACKWARD);
+                        }
+                    }
+                    break;
+            }
+        }
+        return handled;
+    }
+
+    public boolean arrowScroll(int direction) {
+        View currentFocused = findFocus();
+        if (currentFocused == this) {
+            currentFocused = null;
+        } else if (currentFocused != null) {
+            boolean isChild = false;
+            for (ViewParent parent = currentFocused.getParent();
+                    parent instanceof ViewGroup;
+                    parent = parent.getParent()) {
+                if (parent == this) {
+                    isChild = true;
+                    break;
+                }
+            }
+            if (!isChild) {
+                // This would cause the focus search down below to fail in fun ways.
+                final StringBuilder sb = new StringBuilder();
+                sb.append(currentFocused.getClass().getSimpleName());
+                for (ViewParent parent =
+                        currentFocused.getParent();
+                        parent instanceof ViewGroup;
+                        parent = parent.getParent()) {
+                    sb.append(" => ").append(parent.getClass().getSimpleName());
+                }
+                Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
+                        "current focused view " + sb.toString());
+                currentFocused = null;
+            }
+        }
+
+        boolean handled = false;
+
+        View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
+                direction);
+        if (nextFocused != null && nextFocused != currentFocused) {
+            if (direction == View.FOCUS_UP) {
+                // If there is nothing to the left, or this is causing us to
+                // jump to the right, then what we really want to do is page left.
+                final int nextTop = getChildRectInPagerCoordinates(mTempRect, nextFocused).top;
+                final int currTop = getChildRectInPagerCoordinates(mTempRect, currentFocused).top;
+                if (currentFocused != null && nextTop >= currTop) {
+                    handled = pageUp();
+                } else {
+                    handled = nextFocused.requestFocus();
+                }
+            } else if (direction == View.FOCUS_DOWN) {
+                final int nextDown =
+                        getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom;
+                final int currDown =
+                        getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom;
+                if (currentFocused != null && nextDown <= currDown) {
+                    handled = pageDown();
+                } else {
+                    handled = nextFocused.requestFocus();
+                }
+            }
+        } else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) {
+            // Trying to move left and nothing there; try to page.
+            handled = pageUp();
+        } else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) {
+            // Trying to move right and nothing there; try to page.
+            handled = pageDown();
+        }
+        if (handled) {
+            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+        }
+        return handled;
+    }
+
+    private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
+        if (outRect == null) {
+            outRect = new Rect();
+        }
+        if (child == null) {
+            outRect.set(0, 0, 0, 0);
+            return outRect;
+        }
+        outRect.left = child.getLeft();
+        outRect.right = child.getRight();
+        outRect.top = child.getTop();
+        outRect.bottom = child.getBottom();
+
+        ViewParent parent = child.getParent();
+        while (parent instanceof ViewGroup && parent != this) {
+            final ViewGroup group = (ViewGroup) parent;
+            outRect.left += group.getLeft();
+            outRect.right += group.getRight();
+            outRect.top += group.getTop();
+            outRect.bottom += group.getBottom();
+
+            parent = group.getParent();
+        }
+        return outRect;
+    }
+
+    boolean pageUp() {
+        if (mCurItem > 0) {
+            setCurrentItem(mCurItem - 1, true);
+            return true;
+        }
+        return false;
+    }
+
+    boolean pageDown() {
+        if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) {
+            setCurrentItem(mCurItem + 1, true);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * We only want the current page that is being shown to be focusable.
+     */
+    @Override
+    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+        final int focusableCount = views.size();
+
+        final int descendantFocusability = getDescendantFocusability();
+
+        if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
+            for (int i = 0; i < getChildCount(); i++) {
+                final View child = getChildAt(i);
+                if (child.getVisibility() == VISIBLE) {
+                    ItemInfo ii = infoForChild(child);
+                    if (ii != null && ii.position == mCurItem) {
+                        child.addFocusables(views, direction, focusableMode);
+                    }
+                }
+            }
+        }
+
+        // we add ourselves (if focusable) in all cases except for when we are
+        // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
+        // to avoid the focus search finding layouts when a more precise search
+        // among the focusable children would be more interesting.
+        if (
+                descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
+                        // No focusable descendants
+                        (focusableCount == views.size())) {
+            // Note that we can't call the superclass here, because it will
+            // add all views in.  So we need to do the same thing View does.
+            if (!isFocusable()) {
+                return;
+            }
+            if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
+                    isInTouchMode() && !isFocusableInTouchMode()) {
+                return;
+            }
+            if (views != null) {
+                views.add(this);
+            }
+        }
+    }
+
+    /**
+     * We only want the current page that is being shown to be touchable.
+     */
+    @Override
+    public void addTouchables(ArrayList<View> views) {
+        // Note that we don't call super.addTouchables(), which means that
+        // we don't call View.addTouchables().  This is okay because a ViewPager
+        // is itself not touchable.
+        for (int i = 0; i < getChildCount(); i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == VISIBLE) {
+                ItemInfo ii = infoForChild(child);
+                if (ii != null && ii.position == mCurItem) {
+                    child.addTouchables(views);
+                }
+            }
+        }
+    }
+
+    /**
+     * We only want the current page that is being shown to be focusable.
+     */
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction,
+                                                  Rect previouslyFocusedRect) {
+        int index;
+        int increment;
+        int end;
+        int count = getChildCount();
+        if ((direction & FOCUS_FORWARD) != 0) {
+            index = 0;
+            increment = 1;
+            end = count;
+        } else {
+            index = count - 1;
+            increment = -1;
+            end = -1;
+        }
+        for (int i = index; i != end; i += increment) {
+            View child = getChildAt(i);
+            if (child.getVisibility() == VISIBLE) {
+                ItemInfo ii = infoForChild(child);
+                if (ii != null && ii.position == mCurItem &&
+                        child.requestFocus(direction, previouslyFocusedRect)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        // Dispatch scroll events from this ViewPager.
+        if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) {
+            return super.dispatchPopulateAccessibilityEvent(event);
+        }
+
+        // Dispatch all other accessibility events from the current page.
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == VISIBLE) {
+                final ItemInfo ii = infoForChild(child);
+                if (ii != null && ii.position == mCurItem &&
+                        child.dispatchPopulateAccessibilityEvent(event)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams();
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        return generateDefaultLayoutParams();
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof LayoutParams && super.checkLayoutParams(p);
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    class MyAccessibilityDelegate extends AccessibilityDelegateCompat {
+
+        @Override
+        public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+            super.onInitializeAccessibilityEvent(host, event);
+            event.setClassName(ViewPager.class.getName());
+            final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain();
+            recordCompat.setScrollable(canScroll());
+            if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED
+                    && mAdapter != null) {
+                recordCompat.setItemCount(mAdapter.getCount());
+                recordCompat.setFromIndex(mCurItem);
+                recordCompat.setToIndex(mCurItem);
+            }
+        }
+
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+            info.setClassName(ViewPager.class.getName());
+            info.setScrollable(canScroll());
+            if (internalCanScrollVertically(1)) {
+                info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
+            }
+            if (internalCanScrollVertically(-1)) {
+                info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
+            }
+        }
+
+        @Override
+        public boolean performAccessibilityAction(View host, int action, Bundle args) {
+            if (super.performAccessibilityAction(host, action, args)) {
+                return true;
+            }
+            switch (action) {
+                case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {
+                    if (internalCanScrollVertically(1)) {
+                        setCurrentItem(mCurItem + 1);
+                        return true;
+                    }
+                }
+                return false;
+                case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
+                    if (internalCanScrollVertically(-1)) {
+                        setCurrentItem(mCurItem - 1);
+                        return true;
+                    }
+                }
+                return false;
+            }
+            return false;
+        }
+
+        private boolean canScroll() {
+            return (mAdapter != null) && (mAdapter.getCount() > 1);
+        }
+    }
+
+    private class PagerObserver extends DataSetObserver {
+        @Override
+        public void onChanged() {
+            dataSetChanged();
+        }
+
+        @Override
+        public void onInvalidated() {
+            dataSetChanged();
+        }
+    }
+
+    /**
+     * Layout parameters that should be supplied for views added to a
+     * ViewPager.
+     */
+    public static class LayoutParams extends ViewGroup.LayoutParams {
+        /**
+         * true if this view is a decoration on the pager itself and not
+         * a view supplied by the adapter.
+         */
+        public boolean isDecor;
+
+        /**
+         * Gravity setting for use on decor views only:
+         * Where to position the view page within the overall ViewPager
+         * container; constants are defined in {@link Gravity}.
+         */
+        public int gravity;
+
+        /**
+         * Width as a 0-1 multiplier of the measured pager width
+         */
+        float heightFactor = 0.f;
+
+        /**
+         * true if this view was added during layout and needs to be measured
+         * before being positioned.
+         */
+        boolean needsMeasure;
+
+        /**
+         * Adapter position this view is for if !isDecor
+         */
+        int position;
+
+        /**
+         * Current child index within the ViewPager that this view occupies
+         */
+        int childIndex;
+
+        public LayoutParams() {
+            super(FILL_PARENT, FILL_PARENT);
+        }
+
+        public LayoutParams(Context context, AttributeSet attrs) {
+            super(context, attrs);
+
+            final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+            gravity = a.getInteger(0, Gravity.TOP);
+            a.recycle();
+        }
+    }
+
+    static class ViewPositionComparator implements Comparator<View> {
+        @Override
+        public int compare(View lhs, View rhs) {
+            final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
+            final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
+            if (llp.isDecor != rlp.isDecor) {
+                return llp.isDecor ? 1 : -1;
+            }
+            return llp.position - rlp.position;
+        }
+    }
+
+    public class ScrollerCustomDuration extends Scroller {
+
+        private double mScrollFactor = 1;
+
+        public ScrollerCustomDuration(Context context) {
+            super(context);
+        }
+
+        public ScrollerCustomDuration(Context context,
+                                      Interpolator interpolator) {
+            super(context, interpolator);
+        }
+
+        @SuppressLint("NewApi")
+        public ScrollerCustomDuration(Context context,
+                                      Interpolator interpolator, boolean flywheel) {
+            super(context, interpolator, flywheel);
+        }
+
+        /**
+         * Set the factor by which the duration will change
+         */
+        public void setScrollDurationFactor(double scrollFactor) {
+            mScrollFactor = scrollFactor;
+        }
+
+        @Override
+        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+            super.startScroll(startX, startY, dx, dy, (int) (duration * mScrollFactor));
+        }
+
+    }
+}
diff --git a/Android/gradle.properties b/Android/gradle.properties
new file mode 100644 (file)
index 0000000..aac7c9b
--- /dev/null
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/Android/gradle/wrapper/gradle-wrapper.jar b/Android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644 (file)
index 0000000..13372ae
Binary files /dev/null and b/Android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Android/gradle/wrapper/gradle-wrapper.properties b/Android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644 (file)
index 0000000..c182293
--- /dev/null
@@ -0,0 +1,6 @@
+#Tue Apr 10 17:02:46 CEST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/Android/gradlew b/Android/gradlew
new file mode 100755 (executable)
index 0000000..9d82f78
--- /dev/null
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/Android/gradlew.bat b/Android/gradlew.bat
new file mode 100644 (file)
index 0000000..aec9973
--- /dev/null
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off\r
+@rem ##########################################################################\r
+@rem\r
+@rem  Gradle startup script for Windows\r
+@rem\r
+@rem ##########################################################################\r
+\r
+@rem Set local scope for the variables with windows NT shell\r
+if "%OS%"=="Windows_NT" setlocal\r
+\r
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r
+set DEFAULT_JVM_OPTS=\r
+\r
+set DIRNAME=%~dp0\r
+if "%DIRNAME%" == "" set DIRNAME=.\r
+set APP_BASE_NAME=%~n0\r
+set APP_HOME=%DIRNAME%\r
+\r
+@rem Find java.exe\r
+if defined JAVA_HOME goto findJavaFromJavaHome\r
+\r
+set JAVA_EXE=java.exe\r
+%JAVA_EXE% -version >NUL 2>&1\r
+if "%ERRORLEVEL%" == "0" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:findJavaFromJavaHome\r
+set JAVA_HOME=%JAVA_HOME:"=%\r
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe\r
+\r
+if exist "%JAVA_EXE%" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:init\r
+@rem Get command-line arguments, handling Windowz variants\r
+\r
+if not "%OS%" == "Windows_NT" goto win9xME_args\r
+if "%@eval[2+2]" == "4" goto 4NT_args\r
+\r
+:win9xME_args\r
+@rem Slurp the command line arguments.\r
+set CMD_LINE_ARGS=\r
+set _SKIP=2\r
+\r
+:win9xME_args_slurp\r
+if "x%~1" == "x" goto execute\r
+\r
+set CMD_LINE_ARGS=%*\r
+goto execute\r
+\r
+:4NT_args\r
+@rem Get arguments from the 4NT Shell from JP Software\r
+set CMD_LINE_ARGS=%$\r
+\r
+:execute\r
+@rem Setup the command line\r
+\r
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar\r
+\r
+@rem Execute Gradle\r
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r
+\r
+:end\r
+@rem End local scope for the variables with windows NT shell\r
+if "%ERRORLEVEL%"=="0" goto mainEnd\r
+\r
+:fail\r
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r
+rem the _cmd.exe /c_ return code!\r
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1\r
+exit /b 1\r
+\r
+:mainEnd\r
+if "%OS%"=="Windows_NT" endlocal\r
+\r
+:omega\r
diff --git a/Android/r2-streamer/.gitignore b/Android/r2-streamer/.gitignore
new file mode 100755 (executable)
index 0000000..3c44b91
--- /dev/null
@@ -0,0 +1,34 @@
+lt application files
+*.apk
+*.ap_
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated files
+bin/
+gen/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Windows thumbnail db
+Thumbs.db
+
+# OSX files
+.DS_Store
+
+# Eclipse project files
+.classpath
+.project
+
+# Android Studio
+*.iml
+.idea
+#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
+.gradle
+build/
+/sample/src/main/assets/SmokeTestFXL/
diff --git a/Android/r2-streamer/License.txt b/Android/r2-streamer/License.txt
new file mode 100755 (executable)
index 0000000..84f226b
--- /dev/null
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2017, Readium Foundation
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Android/r2-streamer/bintray/bintrayv1.gradle b/Android/r2-streamer/bintray/bintrayv1.gradle
new file mode 100755 (executable)
index 0000000..0328fe8
--- /dev/null
@@ -0,0 +1,59 @@
+apply plugin: 'com.jfrog.bintray'
+
+version = libraryVersion
+
+if (project.hasProperty("android")) { // Android libraries
+    task sourcesJar(type: Jar) {
+        classifier = 'sources'
+        from android.sourceSets.main.java.srcDirs
+    }
+
+//    task javadoc(type: Javadoc) {
+//        source = android.sourceSets.main.java.srcDirs
+//        classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+//    }
+} else { // Java libraries
+    task sourcesJar(type: Jar, dependsOn: classes) {
+        classifier = 'sources'
+        from sourceSets.main.allSource
+    }
+}
+
+//task javadocJar(type: Jar, dependsOn: javadoc) {
+//    classifier = 'javadoc'
+//    from javadoc.destinationDir
+//}
+
+artifacts {
+    //archives javadocJar
+    archives sourcesJar
+}
+
+// Bintray
+Properties properties = new Properties()
+properties.load(project.rootProject.file('local.properties').newDataInputStream())
+
+bintray {
+    user = properties.getProperty("bintray.user")
+    key = properties.getProperty("bintray.apikey")
+
+    configurations = ['archives']
+    pkg {
+        repo = bintrayRepo
+        name = bintrayName
+        desc = libraryDescription
+        websiteUrl = siteUrl
+        vcsUrl = gitUrl
+        licenses = allLicenses
+        publish = true
+        publicDownloadNumbers = true
+        version {
+            desc = libraryDescription
+            gpg {
+                sign = true //Determines whether to GPG sign the files. The default is false
+                passphrase = properties.getProperty("bintray.gpg.password")
+                //Optional. The passphrase for GPG signing'
+            }
+        }
+    }
+}
diff --git a/Android/r2-streamer/bintray/installv1.gradle b/Android/r2-streamer/bintray/installv1.gradle
new file mode 100755 (executable)
index 0000000..b30cd34
--- /dev/null
@@ -0,0 +1,42 @@
+apply plugin: 'com.github.dcendents.android-maven'
+
+group = publishedGroupId                               // Maven Group ID for the artifact
+
+install {
+    repositories.mavenInstaller {
+        // This generates POM.xml with proper parameters
+        pom {
+            project {
+                packaging 'jar'
+                groupId publishedGroupId
+                artifactId artifact
+
+                // Add your description here
+                name libraryName
+                description libraryDescription
+                url siteUrl
+
+                // Set your license
+                licenses {
+                    license {
+                        name licenseName
+                        url licenseUrl
+                    }
+                }
+                developers {
+                    developer {
+                        id developerId
+                        name developerName
+                        email developerEmail
+                    }
+                }
+                scm {
+                    connection gitUrl
+                    developerConnection gitUrl
+                    url siteUrl
+
+                }
+            }
+        }
+    }
+}
diff --git a/Android/r2-streamer/build.gradle b/Android/r2-streamer/build.gradle
new file mode 100755 (executable)
index 0000000..6d3ccab
--- /dev/null
@@ -0,0 +1,25 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        jcenter()
+        google()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.1.2'
+        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4'
+        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1'
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/Android/r2-streamer/config/quality/checkstyle/checkstyle-config.xml b/Android/r2-streamer/config/quality/checkstyle/checkstyle-config.xml
new file mode 100755 (executable)
index 0000000..262a6c3
--- /dev/null
@@ -0,0 +1,167 @@
+<?xml version="1.0"?>\r
+<!DOCTYPE module PUBLIC\r
+    "-//Puppy Crawl//DTD Check Configuration 1.3//EN"\r
+    "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">\r
+\r
+<module name = "Checker">\r
+\r
+    <property name="charset" value="UTF-8"/>\r
+\r
+    <property name="severity" value="error"/>\r
+\r
+    <module name="FileTabCharacter">\r
+        <property name="eachLine" value="true"/>\r
+    </module>\r
+\r
+    <module name="TreeWalker">\r
+\r
+        <!-- Imports -->\r
+\r
+        <module name="RedundantImport">\r
+            <property name="severity" value="error"/>\r
+        </module>\r
+\r
+        <module name="AvoidStarImport">\r
+            <property name="severity" value="error"/>\r
+        </module>\r
+\r
+        <!-- General Code Style -->\r
+\r
+        <module name="LineLength">\r
+            <property name="max" value="100"/>\r
+            <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://|^* static"/>\r
+        </module>\r
+\r
+        <module name="EmptyBlock">\r
+            <property name="option" value="TEXT"/>\r
+            <property name="tokens" value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>\r
+        </module>\r
+\r
+        <module name="EmptyCatchBlock">\r
+            <property name="exceptionVariableName" value="expected"/>\r
+        </module>\r
+\r
+        <module name="LeftCurly">\r
+            <property name="maxLineLength" value="100"/>\r
+        </module>\r
+\r
+        <module name="RightCurly">\r
+            <property name="option" value="alone"/>\r
+            <property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO, STATIC_INIT, INSTANCE_INIT"/>\r
+        </module>\r
+\r
+        <module name="RightCurly">\r
+            <property name="option" value="same"/>\r
+        </module>\r
+\r
+        <module name="NoFinalizer"/>\r
+\r
+        <module name="ArrayTypeStyle"/>\r
+\r
+        <module name="ModifierOrder"/>\r
+\r
+        <module name="Indentation">\r
+            <property name="basicOffset" value="4"/>\r
+            <property name="braceAdjustment" value="0"/>\r
+            <property name="caseIndent" value="4"/>\r
+            <property name="throwsIndent" value="4"/>\r
+            <property name="lineWrappingIndentation" value="8"/>\r
+            <property name="arrayInitIndent" value="2"/>\r
+        </module>\r
+\r
+        <!-- White Space -->\r
+\r
+        <module name="GenericWhitespace">\r
+            <message key="ws.followed"\r
+                     value="GenericWhitespace ''{0}'' is followed by whitespace."/>\r
+            <message key="ws.preceded"\r
+                     value="GenericWhitespace ''{0}'' is preceded with whitespace."/>\r
+            <message key="ws.illegalFollow"\r
+                     value="GenericWhitespace ''{0}'' should followed by whitespace."/>\r
+            <message key="ws.notPreceded"\r
+                     value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>\r
+        </module>\r
+\r
+        <module name="WhitespaceAround">\r
+            <property name="allowEmptyConstructors" value="true"/>\r
+            <property name="allowEmptyMethods" value="false"/>\r
+            <property name="allowEmptyTypes" value="false"/>\r
+            <property name="allowEmptyLoops" value="false"/>\r
+            <message key="ws.notFollowed"\r
+                     value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>\r
+            <message key="ws.notPreceded"\r
+                     value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>\r
+            <property name="severity" value="error"/>\r
+        </module>\r
+\r
+        <module name="WhitespaceAfter">\r
+            <property name="tokens" value="COMMA, SEMI, TYPECAST"/>\r
+        </module>\r
+\r
+        <module name="NoWhitespaceBefore">\r
+            <property name="tokens" value="SEMI, DOT, POST_DEC, POST_INC"/>\r
+            <property name="allowLineBreaks" value="true"/>\r
+        </module>\r
+\r
+        <module name="NoWhitespaceAfter">\r
+            <property name="tokens" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS, UNARY_PLUS"/>\r
+            <property name="allowLineBreaks" value="true"/>\r
+        </module>\r
+\r
+        <!-- Naming -->\r
+\r
+        <module name="PackageName">\r
+            <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Package name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="MethodName">\r
+            <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Method name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="TypeName">\r
+            <message key="name.invalidPattern"\r
+                     value="Type name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="MemberName">\r
+            <property name="applyToPublic" value="false" />\r
+            <property name="applyToPackage" value="false" />\r
+            <property name="applyToProtected" value="false" />\r
+            <property name="format" value="^m[A-Z]+[a-z0-9][a-zA-Z0-9]*$"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Member name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="ParameterName">\r
+            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Parameter name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="LocalVariableName">\r
+            <property name="tokens" value="VARIABLE_DEF"/>\r
+            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>\r
+            <property name="allowOneCharVarInForLoop" value="true"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Local variable name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="ClassTypeParameterName">\r
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Class type name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+        <module name="MethodTypeParameterName">\r
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>\r
+            <message key="name.invalidPattern"\r
+                     value="Method type name ''{0}'' must match pattern ''{1}''."/>\r
+        </module>\r
+\r
+    </module>\r
+\r
+</module>
\ No newline at end of file
diff --git a/Android/r2-streamer/config/quality/findbugs/android-exclude-filter.xml b/Android/r2-streamer/config/quality/findbugs/android-exclude-filter.xml
new file mode 100755 (executable)
index 0000000..b724212
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<FindBugsFilter>
+    <Match>
+        <Class name="~.*\.R\$.*"/>
+    </Match>
+    <Match>
+        <Class name="~.*\.Manifest\$.*"/>
+    </Match>
+    <!-- All bugs in test classes, except for JUnit-specific bugs -->
+    <Match>
+        <Class name="~.*\.*Test" />
+        <Not>
+            <Bug code="IJU" />
+        </Not>
+    </Match>
+
+
+</FindBugsFilter>
\ No newline at end of file
diff --git a/Android/r2-streamer/config/quality/pmd/pmd-ruleset.xml b/Android/r2-streamer/config/quality/pmd/pmd-ruleset.xml
new file mode 100755 (executable)
index 0000000..ad41893
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Android Application Rules"
+    xmlns="http://pmd.sf.net/ruleset/1.0.0"
+    xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"
+    xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd">
+
+    <description>Custom ruleset for ribot Android application</description>
+
+    <exclude-pattern>.*/R.java</exclude-pattern>
+    <exclude-pattern>.*/gen/.*</exclude-pattern>
+
+    <rule ref="rulesets/java/android.xml" />
+    <rule ref="rulesets/java/clone.xml" />
+    <rule ref="rulesets/java/finalizers.xml" />
+    <rule ref="rulesets/java/imports.xml">
+        <!-- Espresso is designed this way !-->
+        <exclude name="TooManyStaticImports" />
+    </rule>
+    <rule ref="rulesets/java/logging-java.xml">
+        <!-- This rule wasn't working properly and given errors in every var call info -->
+        <exclude name="GuardLogStatementJavaUtil" />
+    </rule>
+    <rule ref="rulesets/java/braces.xml">
+        <!-- We allow single line if's without braces -->
+        <exclude name="IfStmtsMustUseBraces" />
+    </rule>
+    <rule ref="rulesets/java/strings.xml" />
+    <rule ref="rulesets/java/basic.xml" />
+    <rule ref="rulesets/java/naming.xml">
+        <exclude name="AbstractNaming" />
+        <exclude name="LongVariable" />
+        <exclude name="ShortMethodName" />
+        <exclude name="ShortVariable" />
+        <exclude name="ShortClassName" />
+        <exclude name="VariableNamingConventions" />
+    </rule>
+</ruleset>
\ No newline at end of file
diff --git a/Android/r2-streamer/config/quality/quality.gradle b/Android/r2-streamer/config/quality/quality.gradle
new file mode 100755 (executable)
index 0000000..e57b911
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Set up Checkstyle, Findbugs and PMD to perform extensive code analysis.
+ *
+ * Gradle tasks added:
+ * - checkstyle
+ * - findbugs
+ * - pmd
+ *
+ * The three tasks above are added as dependencies of the check task so running check will
+ * run all of them.
+ */
+
+apply plugin: 'checkstyle'
+apply plugin: 'findbugs'
+apply plugin: 'pmd'
+
+dependencies {
+    checkstyle 'com.puppycrawl.tools:checkstyle:6.5'
+}
+
+def qualityConfigDir = "$project.rootDir/config/quality";
+def reportsDir = "$project.buildDir/reports"
+
+check.dependsOn 'checkstyle', 'findbugs', 'pmd'
+
+task checkstyle(type: Checkstyle, group: 'Verification', description: 'Runs code style checks') {
+    configFile file("$qualityConfigDir/checkstyle/checkstyle-config.xml")
+    source 'src'
+    include '**/*.java'
+
+    reports {
+        xml.enabled = true
+        xml {
+            destination "$reportsDir/checkstyle/checkstyle.xml"
+        }
+    }
+
+    classpath = files( )
+}
+
+task findbugs(type: FindBugs,
+        group: 'Verification',
+        description: 'Inspect java bytecode for bugs',
+        dependsOn: ['compileDebugSources','compileReleaseSources']) {
+
+    ignoreFailures = false
+    effort = "max"
+    reportLevel = "high"
+    excludeFilter = new File("$qualityConfigDir/findbugs/android-exclude-filter.xml")
+    classes = files("$project.rootDir/folioreader/build/intermediates/classes")
+
+    source 'src'
+    include '**/*.java'
+    exclude '**/gen/**'
+
+    reports {
+        xml.enabled = false
+        html.enabled = true
+        xml {
+            destination "$reportsDir/findbugs/findbugs.xml"
+        }
+        html {
+            destination "$reportsDir/findbugs/findbugs.html"
+        }
+    }
+
+    classpath = files()
+}
+
+
+task pmd(type: Pmd, group: 'Verification', description: 'Inspect sourcecode for bugs') {
+    ruleSetFiles = files("$qualityConfigDir/pmd/pmd-ruleset.xml")
+    ignoreFailures = false
+    ruleSets = []
+
+    source 'src'
+    include '**/*.java'
+    exclude '**/gen/**'
+
+    reports {
+        xml.enabled = true
+        html.enabled = true
+        xml {
+            destination "$reportsDir/pmd/pmd.xml"
+        }
+        html {
+            destination "$reportsDir/pmd/pmd.html"
+        }
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/gradle.properties b/Android/r2-streamer/gradle.properties
new file mode 100755 (executable)
index 0000000..aac7c9b
--- /dev/null
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/Android/r2-streamer/gradle/wrapper/gradle-wrapper.jar b/Android/r2-streamer/gradle/wrapper/gradle-wrapper.jar
new file mode 100755 (executable)
index 0000000..13372ae
Binary files /dev/null and b/Android/r2-streamer/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Android/r2-streamer/gradle/wrapper/gradle-wrapper.properties b/Android/r2-streamer/gradle/wrapper/gradle-wrapper.properties
new file mode 100755 (executable)
index 0000000..a2f31e4
--- /dev/null
@@ -0,0 +1,6 @@
+#Thu May 25 18:26:36 IST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip
diff --git a/Android/r2-streamer/gradlew b/Android/r2-streamer/gradlew
new file mode 100755 (executable)
index 0000000..9d82f78
--- /dev/null
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/Android/r2-streamer/gradlew.bat b/Android/r2-streamer/gradlew.bat
new file mode 100755 (executable)
index 0000000..aec9973
--- /dev/null
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off\r
+@rem ##########################################################################\r
+@rem\r
+@rem  Gradle startup script for Windows\r
+@rem\r
+@rem ##########################################################################\r
+\r
+@rem Set local scope for the variables with windows NT shell\r
+if "%OS%"=="Windows_NT" setlocal\r
+\r
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r
+set DEFAULT_JVM_OPTS=\r
+\r
+set DIRNAME=%~dp0\r
+if "%DIRNAME%" == "" set DIRNAME=.\r
+set APP_BASE_NAME=%~n0\r
+set APP_HOME=%DIRNAME%\r
+\r
+@rem Find java.exe\r
+if defined JAVA_HOME goto findJavaFromJavaHome\r
+\r
+set JAVA_EXE=java.exe\r
+%JAVA_EXE% -version >NUL 2>&1\r
+if "%ERRORLEVEL%" == "0" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:findJavaFromJavaHome\r
+set JAVA_HOME=%JAVA_HOME:"=%\r
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe\r
+\r
+if exist "%JAVA_EXE%" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:init\r
+@rem Get command-line arguments, handling Windowz variants\r
+\r
+if not "%OS%" == "Windows_NT" goto win9xME_args\r
+if "%@eval[2+2]" == "4" goto 4NT_args\r
+\r
+:win9xME_args\r
+@rem Slurp the command line arguments.\r
+set CMD_LINE_ARGS=\r
+set _SKIP=2\r
+\r
+:win9xME_args_slurp\r
+if "x%~1" == "x" goto execute\r
+\r
+set CMD_LINE_ARGS=%*\r
+goto execute\r
+\r
+:4NT_args\r
+@rem Get arguments from the 4NT Shell from JP Software\r
+set CMD_LINE_ARGS=%$\r
+\r
+:execute\r
+@rem Setup the command line\r
+\r
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar\r
+\r
+@rem Execute Gradle\r
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r
+\r
+:end\r
+@rem End local scope for the variables with windows NT shell\r
+if "%ERRORLEVEL%"=="0" goto mainEnd\r
+\r
+:fail\r
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r
+rem the _cmd.exe /c_ return code!\r
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1\r
+exit /b 1\r
+\r
+:mainEnd\r
+if "%OS%"=="Windows_NT" endlocal\r
+\r
+:omega\r
diff --git a/Android/r2-streamer/r2-fetcher/.gitignore b/Android/r2-streamer/r2-fetcher/.gitignore
new file mode 100755 (executable)
index 0000000..796b96d
--- /dev/null
@@ -0,0 +1 @@
+/build
diff --git a/Android/r2-streamer/r2-fetcher/build.gradle b/Android/r2-streamer/r2-fetcher/build.gradle
new file mode 100755 (executable)
index 0000000..6fae579
--- /dev/null
@@ -0,0 +1,40 @@
+apply plugin: 'java'
+
+ext {
+    bintrayRepo = 'maven'
+    bintrayName = 'readium'
+
+    publishedGroupId = 'org.readium'
+    libraryName = 'r2-fetcher'
+    artifact = 'r2-fetcher'
+
+    libraryDescription = 'Library fetches conveys data from parsers to serve'
+
+    siteUrl = 'https://github.com/readium/r2-streamer-java'
+    gitUrl = 'https://github.com/readium/r2-streamer-java.git'
+
+    libraryVersion = '0.1.0'
+
+    developerId = 'mobisystech'
+    developerName = 'CodeToArt'
+    developerEmail = 'mahavir@codetoart.com'
+
+    licenseName = 'FreeBSD License'
+    licenseUrl = 'https://en.wikipedia.org/wiki/FreeBSD_Documentation_License#License'
+    allLicenses = ["FreeBSD"]
+}
+
+dependencies {
+    compile fileTree(include: ['*.jar'], dir: 'libs')
+    compile 'org.jsoup:jsoup:1.10.2'
+    implementation project(':r2-streamer:r2-parser')
+}
+
+sourceCompatibility = "1.7"
+targetCompatibility = "1.7"
+
+apply from: '../bintray/installv1.gradle'
+apply from: '../bintray/bintrayv1.gradle'
+
+
+
diff --git a/Android/r2-streamer/r2-fetcher/src/main/java/org/readium/r2_streamer/fetcher/EpubFetcher.java b/Android/r2-streamer/r2-fetcher/src/main/java/org/readium/r2_streamer/fetcher/EpubFetcher.java
new file mode 100755 (executable)
index 0000000..8013cda
--- /dev/null
@@ -0,0 +1,59 @@
+package org.readium.r2_streamer.fetcher;
+
+import org.readium.r2_streamer.model.container.Container;
+import org.readium.r2_streamer.model.publication.EpubPublication;
+
+import java.io.InputStream;
+
+/**
+ * Created by Shrikant Badwaik on 27-Jan-17.
+ */
+
+public class EpubFetcher implements Fetcher {
+    private final String TAG = "EpubFetcher";
+    public Container container;
+    public EpubPublication publication;
+    private String rootFileDirectory;
+
+    public EpubFetcher(Container container, EpubPublication publication) throws EpubFetcherException {
+        this.container = container;
+        this.publication = publication;
+
+        String rootPath = publication.internalData.get("rootfile");
+        if (rootPath != null) {
+            this.rootFileDirectory = rootPath;
+        } else {
+            throw new EpubFetcherException("No rootFile in internalData, unable to get path to publication");
+        }
+    }
+
+    @Override
+    public String getData(String path) throws EpubFetcherException {
+        String data = container.rawData(path);
+        if (data == null) {
+            System.out.println(TAG + " file is missing " + path);
+            throw new EpubFetcherException(path + " file is missing");
+        }
+        return data;
+    }
+
+    @Override
+    public int getDataSize(String path) throws EpubFetcherException {
+        int dataSize = container.rawDataSize(path);
+        if (dataSize == 0) {
+            System.out.println(TAG + " file is missing " + path);
+            throw new EpubFetcherException(path + "file is missing");
+        }
+        return dataSize;
+    }
+
+    @Override
+    public InputStream getDataInputStream(String path) throws EpubFetcherException {
+        InputStream dataInputStream = container.rawDataInputStream(path);
+        if (dataInputStream == null) {
+            System.out.println(TAG + " file is missing " + path);
+            throw new EpubFetcherException(path + "file is missing");
+        }
+        return dataInputStream;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-fetcher/src/main/java/org/readium/r2_streamer/fetcher/EpubFetcherException.java b/Android/r2-streamer/r2-fetcher/src/main/java/org/readium/r2_streamer/fetcher/EpubFetcherException.java
new file mode 100755 (executable)
index 0000000..97a1afd
--- /dev/null
@@ -0,0 +1,11 @@
+package org.readium.r2_streamer.fetcher;
+
+/**
+ * Created by Shrikant Badwaik on 30-Jan-17.
+ */
+
+public class EpubFetcherException extends Exception {
+    public EpubFetcherException(String message) {
+        super(message);
+    }
+}
diff --git a/Android/r2-streamer/r2-fetcher/src/main/java/org/readium/r2_streamer/fetcher/Fetcher.java b/Android/r2-streamer/r2-fetcher/src/main/java/org/readium/r2_streamer/fetcher/Fetcher.java
new file mode 100755 (executable)
index 0000000..e4d217b
--- /dev/null
@@ -0,0 +1,15 @@
+package org.readium.r2_streamer.fetcher;
+
+import java.io.InputStream;
+
+/**
+ * Created by Shrikant Badwaik on 30-Jan-17.
+ */
+
+public interface Fetcher {
+    String getData(String path) throws EpubFetcherException;
+
+    int getDataSize(String path) throws EpubFetcherException;
+
+    InputStream getDataInputStream(String path) throws EpubFetcherException;
+}
diff --git a/Android/r2-streamer/r2-parser/.gitignore b/Android/r2-streamer/r2-parser/.gitignore
new file mode 100755 (executable)
index 0000000..796b96d
--- /dev/null
@@ -0,0 +1 @@
+/build
diff --git a/Android/r2-streamer/r2-parser/build.gradle b/Android/r2-streamer/r2-parser/build.gradle
new file mode 100755 (executable)
index 0000000..7dd5bea
--- /dev/null
@@ -0,0 +1,41 @@
+apply plugin: 'java'
+
+ext {
+    bintrayRepo = 'maven'
+    bintrayName = 'readium'
+
+    publishedGroupId = 'org.readium'
+    libraryName = 'r2-parser'
+    artifact = 'r2-parser'
+
+    libraryDescription = 'Library parses xml data from EPUB file into java objects'
+
+    siteUrl = 'https://github.com/readium/r2-streamer-java'
+    gitUrl = 'https://github.com/readium/r2-streamer-java.git'
+
+    libraryVersion = '0.1.0'
+
+    developerId = 'mobisystech'
+    developerName = 'CodeToArt'
+    developerEmail = 'mahavir@codetoart.com'
+
+    licenseName = 'FreeBSD License'
+    licenseUrl = 'https://en.wikipedia.org/wiki/FreeBSD_Documentation_License#License'
+    allLicenses = ["FreeBSD"]
+}
+
+dependencies {
+    compile fileTree(include: ['*.jar'], dir: 'libs')
+
+    final JACKSON_VERSION = '2.8.6'
+
+    compile "com.fasterxml.jackson.core:jackson-core:$JACKSON_VERSION"
+    compile "com.fasterxml.jackson.core:jackson-annotations:$JACKSON_VERSION"
+    compile "com.fasterxml.jackson.core:jackson-databind:$JACKSON_VERSION"
+}
+
+sourceCompatibility = "1.7"
+targetCompatibility = "1.7"
+
+apply from: '../bintray/installv1.gradle'
+apply from: '../bintray/bintrayv1.gradle'
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/container/Container.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/container/Container.java
new file mode 100755 (executable)
index 0000000..b2da97a
--- /dev/null
@@ -0,0 +1,18 @@
+package org.readium.r2_streamer.model.container;
+
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Created by Shrikant Badwaik on 24-Jan-17.
+ */
+
+public interface Container {
+    String rawData(String relativePath) throws NullPointerException;
+
+    int rawDataSize(String relativePath);
+
+    List<String> listFiles();
+
+    InputStream rawDataInputStream(String relativePath) throws NullPointerException;
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/container/DirectoryContainer.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/container/DirectoryContainer.java
new file mode 100755 (executable)
index 0000000..0bf888e
--- /dev/null
@@ -0,0 +1,96 @@
+package org.readium.r2_streamer.model.container;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * Created by Shrikant Badwaik on 24-Jan-17.
+ */
+
+public class DirectoryContainer implements Container {
+    private final String TAG = "DirectoryContainer ";
+    private String rootPath;
+
+    public DirectoryContainer(String rootPath) {
+        this.rootPath = rootPath;
+        File epubDirectoryFile = new File(rootPath);
+        if (!epubDirectoryFile.exists()) {
+            System.out.println(TAG + " No such directory exists at path: " + epubDirectoryFile);
+        }
+    }
+
+    @Override
+    public String rawData(String relativePath) throws NullPointerException {
+        String filePath = rootPath.concat(relativePath);
+        File epubFile = new File(filePath);
+
+        if (epubFile.exists()) {
+            System.out.println(TAG + relativePath + " File exists at given path");
+
+            try {
+                InputStream is = new FileInputStream(epubFile);
+                BufferedReader br = new BufferedReader(new InputStreamReader(is));
+                StringBuilder sb = new StringBuilder();
+                String line;
+
+                while ((line = br.readLine()) != null) {
+                    sb.append(line);        //.append('\n');
+                }
+
+                return sb.toString();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        } else if (!epubFile.exists()) {
+            System.out.println(TAG + " No such file exists at given path: " + relativePath);
+        }
+        return null;
+    }
+
+    @Override
+    public int rawDataSize(String relativePath) {
+        String filePath = rootPath.concat(relativePath);
+        File epubFile = new File(filePath);
+        return ((int) epubFile.length());
+    }
+
+    @Override
+    public List<String> listFiles() {
+        return null;
+    }
+
+    @Override
+    public InputStream rawDataInputStream(final String relativePath) throws NullPointerException {
+        try {
+            /*String filePath = rootPath.concat(relativePath);
+            File directoryFile = new File(filePath);
+            InputStream inputStream = new FileInputStream(directoryFile);
+            return inputStream;*/
+
+            Callable<InputStream> callable = new Callable<InputStream>() {
+                @Override
+                public InputStream call() throws Exception {
+                    String filePath = rootPath.concat(relativePath);
+                    File directoryFile = new File(filePath);
+                    return new FileInputStream(directoryFile);
+                }
+            };
+            ExecutorService executorService = Executors.newCachedThreadPool();
+            Future<InputStream> future = executorService.submit(callable);
+            return future.get();
+        } catch (InterruptedException | ExecutionException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/container/EpubContainer.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/container/EpubContainer.java
new file mode 100755 (executable)
index 0000000..3aeb9c0
--- /dev/null
@@ -0,0 +1,118 @@
+package org.readium.r2_streamer.model.container;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Created by Shrikant Badwaik on 24-Jan-17.
+ */
+
+public class EpubContainer implements Container {
+    private final String TAG = "EpubContainer";
+    private ZipFile zipFile;
+
+    public EpubContainer(String epubFilePath) throws IOException {
+        this.zipFile = new ZipFile(epubFilePath);
+
+        System.out.println(TAG + " Reading epub at path: " + epubFilePath);
+    }
+
+    @Override
+    public String rawData(String relativePath) throws NullPointerException {
+        System.out.println(TAG + " Reading file at path: " + relativePath);
+        try {
+            ZipEntry zipEntry = zipFile.getEntry(relativePath);
+            if (zipEntry == null) {
+                return "";
+            }
+            InputStream is = zipFile.getInputStream(zipEntry);
+            BufferedReader br = new BufferedReader(new InputStreamReader(is));
+            StringBuilder sb = new StringBuilder();
+            String line;
+
+            while ((line = br.readLine()) != null) {
+                sb.append(line);        //.append('\n');
+            }
+
+            return sb.toString();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    @Override
+    public int rawDataSize(String relativePath) {
+        ZipEntry zipEntry = zipFile.getEntry(relativePath);
+        return ((int) zipEntry.getSize());
+    }
+
+    @Override
+    public List<String> listFiles() {
+        List<String> files = new ArrayList<>();
+        Enumeration zipEntries = zipFile.entries();
+        while (zipEntries.hasMoreElements()) {
+            String fileName = ((ZipEntry) zipEntries.nextElement()).getName();
+            files.add(fileName);
+        }
+        return files;
+    }
+
+    @Override
+    public InputStream rawDataInputStream(final String relativePath) throws NullPointerException {
+        try {
+            //ZipEntry zipEntry = zipFile.getEntry(relativePath);
+            /*InputStream inputStream = zipFile.getInputStream(zipEntry);
+            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            int bytesRead;
+            byte[] byteArray = new byte[4069];
+            while ((bytesRead = inputStream.read(byteArray)) != -1){
+                byteArrayOutputStream.write(byteArray, 0, bytesRead);
+            }
+
+            byteArrayOutputStream.flush();
+            byte[] streamArray = byteArrayOutputStream.toByteArray();
+            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(streamArray);*/
+
+            Callable<ByteArrayInputStream> callable = new Callable<ByteArrayInputStream>() {
+                @Override
+                public ByteArrayInputStream call() throws Exception {
+                    ZipEntry zipEntry = zipFile.getEntry(relativePath);
+                    InputStream inputStream = zipFile.getInputStream(zipEntry);
+                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+                    long BUFFER_SIZE = 16 * 1024;
+                    byte[] byteArray = new byte[(int) BUFFER_SIZE];
+                    int bytesRead;
+                    while ((bytesRead = inputStream.read(byteArray)) != -1) {
+                        byteArrayOutputStream.write(byteArray, 0, bytesRead);
+                    }
+
+                    byteArrayOutputStream.flush();
+                    byte[] streamArray = byteArrayOutputStream.toByteArray();
+                    return new ByteArrayInputStream(streamArray);
+                }
+            };
+
+            ExecutorService executorService = Executors.newCachedThreadPool();
+            Future<ByteArrayInputStream> future = executorService.submit(callable);
+            return future.get();
+        } catch (InterruptedException | ExecutionException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/Encryption.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/Encryption.java
new file mode 100755 (executable)
index 0000000..568b0a8
--- /dev/null
@@ -0,0 +1,83 @@
+package org.readium.r2_streamer.model.publication;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Created by gautam chibde on 18/5/17.
+ */
+
+public class Encryption implements Serializable {
+    private static final long serialVersionUID = 333647343242776147L;
+
+    private String scheme;
+    private String profile;
+    private String algorithm;
+    private String compression;
+    private int originalLength;
+
+    public Encryption() {
+    }
+
+    @Override
+    public String toString() {
+        return "Encryption{" +
+                "scheme='" + scheme + '\'' +
+                ", profile='" + profile + '\'' +
+                ", algorithm='" + algorithm + '\'' +
+                ", compression='" + compression + '\'' +
+                ", originalLength=" + originalLength +
+                '}';
+    }
+
+    public static Encryption getEncryptionFormFontFilePath(
+            String path,
+            List<Encryption> encryptions) {
+        for (Encryption encryption : encryptions) {
+            if (encryption.getProfile().equalsIgnoreCase(path)) {
+                return encryption;
+            }
+        }
+        return null;
+    }
+
+    public String getScheme() {
+        return scheme;
+    }
+
+    public void setScheme(String scheme) {
+        this.scheme = scheme;
+    }
+
+    public String getProfile() {
+        return profile;
+    }
+
+    public void setProfile(String profile) {
+        this.profile = profile;
+    }
+
+    public String getAlgorithm() {
+        return algorithm;
+    }
+
+    public void setAlgorithm(String algorithm) {
+        this.algorithm = algorithm;
+    }
+
+    public String getCompression() {
+        return compression;
+    }
+
+    public void setCompression(String compression) {
+        this.compression = compression;
+    }
+
+    public int getOriginalLength() {
+        return originalLength;
+    }
+
+    public void setOriginalLength(int originalLength) {
+        this.originalLength = originalLength;
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/EpubPublication.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/EpubPublication.java
new file mode 100755 (executable)
index 0000000..88208e6
--- /dev/null
@@ -0,0 +1,115 @@
+package org.readium.r2_streamer.model.publication;
+
+import org.readium.r2_streamer.model.publication.link.Link;
+import org.readium.r2_streamer.model.publication.metadata.MetaData;
+import org.readium.r2_streamer.model.tableofcontents.TOCLink;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public class EpubPublication implements Serializable{
+    private static final long serialVersionUID = 3336472295622776147L;
+
+    @JsonProperty("metadata")
+    public MetaData metadata;
+
+    @JsonIgnore
+    public HashMap<String,Link> linkMap;
+    @JsonProperty("links")
+    public List<Link> links;
+    @JsonIgnore
+    public List<Link> matchingLinks;
+    @JsonProperty("spines")
+    public List<Link> spines;
+    @JsonProperty("resources")
+    public List<Link> resources;
+    @JsonIgnore
+    public List<Link> guides;
+
+    @JsonIgnore
+    public List<Encryption> encryptions;
+
+    //public List<Link> pageList;
+    @JsonProperty("toc")
+    public List<TOCLink> tableOfContents;
+
+    @JsonProperty
+    public List<TOCLink> pageList;
+    @JsonIgnore
+    //public List<Link> landmarks;
+    public Link[] landmarks;
+    @JsonIgnore
+    //public List<Link> LOI;
+    public Link[] LOI;
+    @JsonIgnore
+    //public List<Link> LOA;
+    public Link[] LOA;
+    @JsonIgnore
+    //public List<Link> LOV;
+    public Link[] LOV;
+    @JsonIgnore
+    //public List<Link> LOT;
+    public Link[] LOT;
+
+    public HashMap<String, String> internalData;
+
+    @JsonIgnore
+    //public List<Link> otherLinks;
+    public Link[] otherLinks;
+
+    @JsonProperty("cover")
+    public Link coverLink;
+
+    public EpubPublication() {
+        this.matchingLinks = new ArrayList<>();
+        this.links = new ArrayList<>();
+        this.spines = new ArrayList<>();
+        this.encryptions = new ArrayList<>();
+        this.resources = new ArrayList<>();
+        this.guides= new ArrayList<>();
+        this.internalData = new HashMap<>();
+
+        this.linkMap = new HashMap<>();
+    }
+
+    @Override
+    public String toString() {
+        return "EpubPublication{" +
+                "metadata=" + metadata +
+                ", tableOfContents=" + tableOfContents +
+                ", linkMap=" + linkMap +
+                ", links=" + links +
+                ", matchingLinks=" + matchingLinks +
+                ", spines=" + spines +
+                ", encryptions=" + encryptions +
+                ", resources=" + resources +
+                ", guides=" + guides +
+                ", pageList=" + pageList +
+                ", landmarks=" + Arrays.toString(landmarks) +
+                ", LOI=" + Arrays.toString(LOI) +
+                ", LOA=" + Arrays.toString(LOA) +
+                ", LOV=" + Arrays.toString(LOV) +
+                ", LOT=" + Arrays.toString(LOT) +
+                ", internalData=" + internalData +
+                ", otherLinks=" + Arrays.toString(otherLinks) +
+                ", coverLink=" + coverLink +
+                '}';
+    }
+
+    public Link getResourceMimeType(String resourcePath) {
+        if(linkMap.containsKey(resourcePath)){
+            return linkMap.get(resourcePath);
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/Clip.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/Clip.java
new file mode 100755 (executable)
index 0000000..8147946
--- /dev/null
@@ -0,0 +1,32 @@
+package org.readium.r2_streamer.model.publication.SMIL;
+
+import java.io.Serializable;
+
+/**
+ * Created by gautam chibde on 23/5/17.
+ */
+
+public class Clip implements Serializable {
+    private static final long serialVersionUID = -3313414920068632537L;
+
+    public String relativeUrl;
+
+    public double start;
+
+    public double end;
+
+    public double duration;
+
+    public Clip() {
+    }
+
+    @Override
+    public String toString() {
+        return "Clip{" +
+                "relativeUrl='" + relativeUrl + '\'' +
+                ", start=" + start +
+                ", end=" + end +
+                ", duration=" + duration +
+                '}';
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/MediaOverlayNode.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/MediaOverlayNode.java
new file mode 100755 (executable)
index 0000000..a9e969f
--- /dev/null
@@ -0,0 +1,69 @@
+package org.readium.r2_streamer.model.publication.SMIL;
+
+import org.w3c.dom.Element;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by gautam chibde on 23/5/17.
+ */
+
+public class MediaOverlayNode implements Serializable {
+    private static final long serialVersionUID = 7329984331545950872L;
+
+    public String text;
+    public String audio;
+    public List<String> role;
+    public List<MediaOverlayNode> children;
+
+    public MediaOverlayNode() {
+        this.children = new ArrayList<>();
+        this.role = new ArrayList<>();
+    }
+
+    @Override
+    public String toString() {
+        return "MediaOverlayNode{" +
+                "text='" + text + '\'' +
+                ", audio='" + audio + '\'' +
+                ", role=" + role +
+                ", children=" + children +
+                '}';
+    }
+
+    /**
+     * Generate Clip from current instance object
+     *
+     * @return The generated Clip.
+     */
+    public Clip clip() throws IndexOutOfBoundsException {
+        Clip newClip = new Clip();
+
+        // Retrieve the audioString (containing timers + audiofile url), then
+        // retrieve both.
+        newClip.relativeUrl = this.audio.split("#")[0];
+        String times = this.audio.split("#")[1];
+        return parseTimer(times, newClip);
+    }
+
+    /**
+     * Parse the time String to fill clip.
+     *
+     * @param times The time string ("t=S.MS,S.MS") as created in {@link SMILParser#parseAudio(Element)}
+     * @param clip  The Clip instance where to fill the parsed data.
+     * @return returns clips with start, end and duration
+     */
+    private Clip parseTimer(String times, Clip clip) throws IndexOutOfBoundsException {
+        // Remove "t=" prefix from times string.
+        times = times.substring(2, times.length());
+        // Parse start and end times.
+        Double start = Double.parseDouble(times.split(",")[0]);
+        Double end = Double.parseDouble(times.split(",")[1]);
+        clip.start = start;
+        clip.end = end;
+        clip.duration = end - start;
+        return clip;
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/MediaOverlays.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/MediaOverlays.java
new file mode 100755 (executable)
index 0000000..efb4b0c
--- /dev/null
@@ -0,0 +1,121 @@
+package org.readium.r2_streamer.model.publication.SMIL;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by gautam chibde on 23/5/17.
+ */
+
+public class MediaOverlays implements Serializable {
+
+    private static final long serialVersionUID = 335418192699543070L;
+
+    @JsonProperty("media-overlay")
+    public List<MediaOverlayNode> mediaOverlayNodes;
+
+    public MediaOverlays() {
+        this.mediaOverlayNodes = new ArrayList<>();
+    }
+
+    @Override
+    public String toString() {
+        return "MediaOverlays{" +
+                ", mediaOverlayNodes=" + mediaOverlayNodes +
+                '}';
+    }
+
+    /**
+     * Function return the path of the audio file for the
+     * given page href
+     *
+     * @param href page href
+     * @return audio file path for given SMIL
+     */
+    public String getAudioPath(String href) {
+        // extract file name
+        if (href.contains("/")) {
+            int startIndex = href.lastIndexOf("/");
+            href = href.substring(startIndex + 1, href.length());
+        }
+        String path = findAudioPath(href, this.mediaOverlayNodes);
+        if (path != null) {
+            return path;
+        }
+        return null;
+    }
+
+    /**
+     * [RECURSIVE]
+     * <p>
+     * Return the file path from the first node element
+     *
+     * @param href  href of page
+     * @param nodes media overlay nodes
+     * @return audio file path
+     */
+    private String findAudioPath(String href,
+                                 List<MediaOverlayNode> nodes) {
+        // For each node of the current scope..
+        for (MediaOverlayNode node : nodes) {
+            if (node.audio != null) {
+                if (node.text.contains(href)) {
+                    if (node.audio.contains("#")) {
+                        return node.audio.split("#")[0];
+                    }
+                }
+            }
+            if (node.role.contains("section")) {
+                return findAudioPath(href, node.children);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * <p>
+     * Get the audio `Clip` associated to an audio Fragment id.
+     * The fragment id can be found in the HTML document in <p> & <span> tags,
+     * it refer to a element of one of the SMIL files, providing information
+     * This function returns the clip representing this element from SMIL.
+     * about the synchronized audio.
+     * </p>
+     *
+     * @param forFragmentId The audio fragment id.
+     * @return The `Clip`, representation of the associated SMIL element.
+     */
+    public Clip clip(String forFragmentId) {
+        MediaOverlayNode node = findNode(forFragmentId, this.mediaOverlayNodes);
+        if (node != null) {
+            return node.clip();
+        }
+        return new Clip();
+    }
+
+    /**
+     * [RECURSIVE]
+     * <p>
+     * Find the node (<par>) corresponding to "fragment" ?? nil.
+     * </p>
+     *
+     * @param fragment The current fragment name for which we are looking the
+     * @param nodes    The set of MediaOverlayNodes where to search. Default to  self children.
+     * @return node corresponding to the fragment id, null if not found
+     */
+    private MediaOverlayNode findNode(String fragment,
+                                      List<MediaOverlayNode> nodes) {
+        // For each node of the current scope..
+        for (MediaOverlayNode node : nodes) {
+            if (node.text.contains(fragment)) {
+                return node;
+            }
+            if (node.role.contains("section")) {
+                return findNode(fragment, node.children);
+            }
+        }
+        return null;
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/SMILParser.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/SMIL/SMILParser.java
new file mode 100755 (executable)
index 0000000..b92ef80
--- /dev/null
@@ -0,0 +1,196 @@
+package org.readium.r2_streamer.model.publication.SMIL;
+
+import org.w3c.dom.Element;
+
+/**
+ * Created by gautam chibde on 23/5/17.
+ */
+
+public final class SMILParser {
+
+    //Describes the different time string format of the SMIL tags
+    private enum SMILTimeFormat {
+        SPLIT_MONADIC,
+        SPLIT_DYADIC,
+        SPLIT_TRIADIC,
+        MILLISECOND,
+        SECOND,
+        HOUR,
+    }
+
+    /**
+     * Converts a smile time string into seconds String.
+     *
+     * @param time The smile time String.
+     * @return The converted value in Seconds as String.
+     */
+    public static String smilTimeToSeconds(String time) {
+        if (time.contains("h")) {
+            return convertToSeconds(time, SMILTimeFormat.HOUR);
+        } else if (time.contains("s")) {
+            return convertToSeconds(time, SMILTimeFormat.SECOND);
+
+        } else if (time.contains("ms")) {
+            return convertToSeconds(time, SMILTimeFormat.MILLISECOND);
+        } else {
+            int count = time.split(":").length;
+            switch (count) {
+                case 1:
+                    return convertToSeconds(time, SMILTimeFormat.SPLIT_MONADIC);
+                case 2:
+                    return convertToSeconds(time, SMILTimeFormat.SPLIT_DYADIC);
+                case 3:
+                    return convertToSeconds(time, SMILTimeFormat.SPLIT_TRIADIC);
+                default:
+                    return ""; // Should return null?
+            }
+        }
+    }
+
+    /**
+     * Convert the smileTime to the equivalent in seconds given it's type.
+     *
+     * @param time The SMILTime String.
+     * @param type type format of smileTime
+     * @return The converted value in Seconds as String.
+     */
+    private static String convertToSeconds(String time, SMILTimeFormat type) {
+        double seconds = 0.0;
+        switch (type) {
+            case HOUR:
+                double ms = Double.parseDouble(time.replaceAll("ms", ""));
+                return String.valueOf(ms / 1000.0);
+            case SECOND:
+                return time.replaceAll("s", "");
+            case MILLISECOND:
+                String[] hourMin = time.split(time.replaceAll("h", ""));
+                double hrToSec = Double.parseDouble(hourMin[0]) * 3600.0;
+                double minToSec = Double.parseDouble(hourMin[1]) * 0.6 * 60.0;
+                return String.valueOf(hrToSec + minToSec);
+            case SPLIT_MONADIC:
+                return time;
+            case SPLIT_DYADIC:
+                String[] minSec = time.split(":");
+                seconds += Double.parseDouble(minSec[0]) * 60.0;
+                seconds += parseSeconds(time);
+                return String.valueOf(seconds);
+            case SPLIT_TRIADIC:
+                String[] hourMinSec = time.split(":");
+
+                seconds += (Double.parseDouble(hourMinSec[0])) * 3600.0;
+
+                seconds += (Double.parseDouble(hourMinSec[1])) * 60.0;
+
+                seconds += parseSeconds(hourMinSec[2]);
+                return String.valueOf(seconds);
+            default:
+                return "";
+        }
+    }
+
+    /**
+     * Parse the <audio> XML element, children of <par> elements.
+     *
+     * @param element The audio XML element.
+     * @return The formatted string representing the data
+     * format => audio_path#t=start_time,end_time.
+     */
+    public static String parseAudio(Element element, String href) {
+        String audio, clipBegin, clipEnd;
+        if (element.hasAttribute("src")) {
+            audio = element.getAttribute("src");
+        } else {
+            return null;
+        }
+        if (element.hasAttribute("clipBegin")) {
+            clipBegin = element.getAttribute("clipBegin");
+        } else {
+            return null;
+        }
+        if (element.hasAttribute("clipEnd")) {
+            clipEnd = element.getAttribute("clipEnd");
+        } else {
+            return null;
+        }
+
+        return getAbsoluteUriPath(href, audio) +
+                "#t=" +
+                smilTimeToSeconds(clipBegin) +
+                "," +
+                smilTimeToSeconds(clipEnd);
+    }
+
+    /**
+     * function creates absolute URI path for the audio file.
+     * in ref => https://github.com/readium/readium-2/issues/38.
+     * <p>
+     * TODO check with other Epub file. specifically with deep hierarchy
+     *
+     * @param href  path of SMIL file.
+     * @param audio audio path in SMIL file to update.
+     * @return absolute URI path.
+     */
+    private static String getAbsoluteUriPath(String href, String audio) {
+        StringBuilder toAppend = new StringBuilder();
+        if (href.contains("/")) {
+            // removes file name from path
+            int startIndex = href.lastIndexOf("/");
+            String filePath = href.substring(0, startIndex);
+
+            int count = countSubString(audio, "../");
+            String[] items = filePath.split("/");
+
+            for (int i = 0; i < count; i++) {
+                if (items.length > i) {
+                    if ((items.length - i - 2) >= 0) {
+                        toAppend.insert(0, items[items.length - i - 2] + "/");
+                    }
+                }
+            }
+        }
+        if (audio.contains("../")) {
+            audio = audio.replace("../", "");
+        }
+        return toAppend.toString() + audio;
+    }
+
+    /**
+     * Return the seconds double value from a possible SS.MS format.
+     *
+     * @param time The seconds String.
+     * @return The translated Double value.
+     */
+    private static double parseSeconds(String time) {
+        String[] sec = time.split(":");
+        double seconds;
+        if (sec.length == 2) {
+            seconds = Double.parseDouble(sec[0]);
+            seconds += (Double.parseDouble(sec[1])) / 1000.0;
+        } else {
+            seconds = Double.parseDouble(time);
+        }
+        return seconds;
+    }
+
+    /**
+     * Returns number of occurrences of string b in string a.
+     *
+     * @param a string in which to find occurrences.
+     * @param b input substring
+     * @return count
+     */
+    private static int countSubString(String a, String b) {
+        int lastIndex = 0;
+        int count = 0;
+        while (lastIndex != -1) {
+
+            lastIndex = a.indexOf(b, lastIndex);
+
+            if (lastIndex != -1) {
+                count++;
+                lastIndex += b.length();
+            }
+        }
+        return count;
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/contributor/Contributor.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/contributor/Contributor.java
new file mode 100755 (executable)
index 0000000..853ee08
--- /dev/null
@@ -0,0 +1,64 @@
+package org.readium.r2_streamer.model.publication.contributor;
+
+import java.io.Serializable;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public class Contributor implements Serializable {
+    private static final long serialVersionUID = 7666462295622776147L;
+    public String name;
+    public String sortAs;
+    public String identifier;
+    public String role;
+
+    public Contributor() {
+    }
+
+    @Override
+    public String toString() {
+        return "Contributor{" +
+                "name='" + name + '\'' +
+                ", sortAs='" + sortAs + '\'' +
+                ", identifier='" + identifier + '\'' +
+                ", role='" + role + '\'' +
+                '}';
+    }
+
+    public Contributor(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getSortAs() {
+        return sortAs;
+    }
+
+    public void setSortAs(String sortAs) {
+        this.sortAs = sortAs;
+    }
+
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    public String getRole() {
+        return role;
+    }
+
+    public void setRole(String role) {
+        this.role = role;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/link/Link.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/link/Link.java
new file mode 100755 (executable)
index 0000000..a3e1dac
--- /dev/null
@@ -0,0 +1,154 @@
+package org.readium.r2_streamer.model.publication.link;
+
+import org.readium.r2_streamer.model.publication.SMIL.MediaOverlays;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public class Link implements Serializable {
+    private static final long serialVersionUID = 7612342295622776147L;
+    public String id;
+    public String href;
+    public List<String> rel = new ArrayList<>();
+    public String typeLink;
+    public int height;
+    public int width;
+    public String bookTitle;
+    public String chapterTitle;
+    public String type;
+    public List<String> properties;
+    public String duration;
+    public boolean templated;
+    public MediaOverlays mediaOverlay;
+
+    public Link() {
+        this.properties = new ArrayList<>();
+        this.mediaOverlay = new MediaOverlays();
+    }
+
+    public Link(String href, String rel, String typeLink) {
+        this.href = href;
+        this.rel.add(rel);
+        this.typeLink = typeLink;
+    }
+
+    @Override
+    public String toString() {
+        return "Link{" +
+                "id='" + id + '\'' +
+                ", mediaOverlay=" + mediaOverlay +
+                ", href='" + href + '\'' +
+                ", rel=" + rel +
+                ", typeLink='" + typeLink + '\'' +
+                ", height=" + height +
+                ", width=" + width +
+                ", bookTitle='" + bookTitle + '\'' +
+                ", chapterTitle='" + chapterTitle + '\'' +
+                ", type='" + type + '\'' +
+                ", properties=" + properties +
+                ", duration='" + duration + '\'' +
+                ", templated=" + templated +
+                '}';
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getHref() {
+        return href;
+    }
+
+    public void setHref(String href) {
+        this.href = href;
+    }
+
+    public List<String> getRel() {
+        return rel;
+    }
+
+    public void setRel(List<String> rel) {
+        this.rel = rel;
+    }
+
+    public String getTypeLink() {
+        return typeLink;
+    }
+
+    public void setTypeLink(String typeLink) {
+        this.typeLink = typeLink;
+    }
+
+    public int getHeight() {
+        return height;
+    }
+
+    public void setHeight(int height) {
+        this.height = height;
+    }
+
+    public int getWidth() {
+        return width;
+    }
+
+    public void setWidth(int width) {
+        this.width = width;
+    }
+
+    public String getBookTitle() {
+        return bookTitle;
+    }
+
+    public void setBookTitle(String bookTitle) {
+        this.bookTitle = bookTitle;
+    }
+
+    public String getChapterTitle() {
+        return chapterTitle;
+    }
+
+    public void setChapterTitle(String chapterTitle) {
+        this.chapterTitle = chapterTitle;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public List<String> getProperties() {
+        return properties;
+    }
+
+    public void setProperties(List<String> properties) {
+        this.properties = properties;
+    }
+
+    public String getDuration() {
+        return duration;
+    }
+
+    public void setDuration(String duration) {
+        this.duration = duration;
+    }
+
+    public boolean isTemplated() {
+        return templated;
+    }
+
+    public void setTemplated(boolean templated) {
+        this.templated = templated;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/metadata/MetaData.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/metadata/MetaData.java
new file mode 100755 (executable)
index 0000000..5eac667
--- /dev/null
@@ -0,0 +1,338 @@
+package org.readium.r2_streamer.model.publication.metadata;
+
+import org.readium.r2_streamer.model.publication.subject.Subject;
+import org.readium.r2_streamer.model.publication.contributor.Contributor;
+import org.readium.r2_streamer.model.publication.rendition.Rendition;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public class MetaData implements Serializable {
+    private static final long serialVersionUID = 8526472295622776148L;
+    public String title;
+    public String identifier;
+
+    public List<Contributor> creators;
+    public List<Contributor> translators;
+    public List<Contributor> editors;
+    public List<Contributor> artists;
+    public List<Contributor> illustrators;
+    public List<Contributor> letterers;
+    public List<Contributor> pencilers;
+    public List<Contributor> colorists;
+    public List<Contributor> inkers;
+    public List<Contributor> narrators;
+    public List<Contributor> contributors;
+    public List<Contributor> publishers;
+    public List<Contributor> imprints;
+
+    public List<String> languages;
+    public Date modified;
+    public Date publicationDate;
+    public String description;
+    public String direction;
+    public Rendition rendition;
+    public String source;
+    public List<String> epubType;
+    public List<String> rights;
+    public List<Subject> subjects;
+
+    private List<MetadataItem> otherMetadata;
+
+    public MetaData() {
+        this.rendition = new Rendition();
+        this.creators = new ArrayList<>();
+        this.translators = new ArrayList<>();
+        this.editors = new ArrayList<>();
+        this.artists = new ArrayList<>();
+        this.illustrators = new ArrayList<>();
+        this.letterers = new ArrayList<>();
+        this.pencilers = new ArrayList<>();
+        this.colorists = new ArrayList<>();
+        this.inkers = new ArrayList<>();
+        this.narrators = new ArrayList<>();
+        this.contributors = new ArrayList<>();
+        this.publishers = new ArrayList<>();
+        this.imprints = new ArrayList<>();
+        this.languages = new ArrayList<>();
+        this.epubType = new ArrayList<>();
+        this.rights = new ArrayList<>();
+        this.subjects = new ArrayList<>();
+        this.otherMetadata = new ArrayList<>();
+    }
+
+    public MetaData(String title, String identifier, List<Contributor> creators, List<Contributor> translators, List<Contributor> editors, List<Contributor> artists, List<Contributor> illustrators, List<Contributor> letterers, List<Contributor> pencilers, List<Contributor> colorists, List<Contributor> inkers, List<Contributor> narrators, List<Contributor> contributors, List<Contributor> publishers, List<Contributor> imprints, List<String> languages, Date modified, Date publicationDate, String description, String direction, Rendition rendition, String source, List<String> epubType, List<String> rights, List<Subject> subjects, List<MetadataItem> otherMetadata) {
+        this.title = title;
+        this.identifier = identifier;
+        this.creators = creators;
+        this.translators = translators;
+        this.editors = editors;
+        this.artists = artists;
+        this.illustrators = illustrators;
+        this.letterers = letterers;
+        this.pencilers = pencilers;
+        this.colorists = colorists;
+        this.inkers = inkers;
+        this.narrators = narrators;
+        this.contributors = contributors;
+        this.publishers = publishers;
+        this.imprints = imprints;
+        this.languages = languages;
+        this.modified = modified;
+        this.publicationDate = publicationDate;
+        this.description = description;
+        this.direction = "default";     // = direction;
+        this.rendition = rendition;
+        this.source = source;
+        this.epubType = epubType;
+        this.rights = rights;
+        this.subjects = subjects;
+        this.otherMetadata = otherMetadata;
+    }
+
+    @Override
+    public String toString() {
+        return "MetaData{" +
+                "title='" + title + '\'' +
+                ", identifier='" + identifier + '\'' +
+                ", creators=" + creators +
+                ", translators=" + translators +
+                ", editors=" + editors +
+                ", artists=" + artists +
+                ", illustrators=" + illustrators +
+                ", letterers=" + letterers +
+                ", pencilers=" + pencilers +
+                ", colorists=" + colorists +
+                ", inkers=" + inkers +
+                ", narrators=" + narrators +
+                ", contributors=" + contributors +
+                ", publishers=" + publishers +
+                ", imprints=" + imprints +
+                ", languages=" + languages +
+                ", modified=" + modified +
+                ", publicationDate=" + publicationDate +
+                ", description='" + description + '\'' +
+                ", direction='" + direction + '\'' +
+                ", rendition=" + rendition +
+                ", source='" + source + '\'' +
+                ", epubType=" + epubType +
+                ", rights=" + rights +
+                ", subjects=" + subjects +
+                ", otherMetadata=" + otherMetadata +
+                '}';
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    public List<Contributor> getCreators() {
+        return creators;
+    }
+
+    public void setCreators(List<Contributor> creators) {
+        this.creators = creators;
+    }
+
+    public List<Contributor> getTranslators() {
+        return translators;
+    }
+
+    public void setTranslators(List<Contributor> translators) {
+        this.translators = translators;
+    }
+
+    public List<Contributor> getEditors() {
+        return editors;
+    }
+
+    public void setEditors(List<Contributor> editors) {
+        this.editors = editors;
+    }
+
+    public List<Contributor> getArtists() {
+        return artists;
+    }
+
+    public void setArtists(List<Contributor> artists) {
+        this.artists = artists;
+    }
+
+    public List<Contributor> getIllustrators() {
+        return illustrators;
+    }
+
+    public void setIllustrators(List<Contributor> illustrators) {
+        this.illustrators = illustrators;
+    }
+
+    public List<Contributor> getLetterers() {
+        return letterers;
+    }
+
+    public void setLetterers(List<Contributor> letterers) {
+        this.letterers = letterers;
+    }
+
+    public List<Contributor> getPencilers() {
+        return pencilers;
+    }
+
+    public void setPencilers(List<Contributor> pencilers) {
+        this.pencilers = pencilers;
+    }
+
+    public List<Contributor> getColorists() {
+        return colorists;
+    }
+
+    public void setColorists(List<Contributor> colorists) {
+        this.colorists = colorists;
+    }
+
+    public List<Contributor> getInkers() {
+        return inkers;
+    }
+
+    public void setInkers(List<Contributor> inkers) {
+        this.inkers = inkers;
+    }
+
+    public List<Contributor> getNarrators() {
+        return narrators;
+    }
+
+    public void setNarrators(List<Contributor> narrators) {
+        this.narrators = narrators;
+    }
+
+    public List<Contributor> getContributors() {
+        return contributors;
+    }
+
+    public void setContributors(List<Contributor> contributors) {
+        this.contributors = contributors;
+    }
+
+    public List<Contributor> getPublishers() {
+        return publishers;
+    }
+
+    public void setPublishers(List<Contributor> publishers) {
+        this.publishers = publishers;
+    }
+
+    public List<Contributor> getImprints() {
+        return imprints;
+    }
+
+    public void setImprints(List<Contributor> imprints) {
+        this.imprints = imprints;
+    }
+
+    public List<String> getLanguages() {
+        return languages;
+    }
+
+    public void setLanguages(List<String> languages) {
+        this.languages = languages;
+    }
+
+    public Date getModified() {
+        return modified;
+    }
+
+    public void setModified(Date modified) {
+        this.modified = modified;
+    }
+
+    public Date getPublicationDate() {
+        return publicationDate;
+    }
+
+    public void setPublicationDate(Date publicationDate) {
+        this.publicationDate = publicationDate;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getDirection() {
+        return direction;
+    }
+
+    public void setDirection(String direction) {
+        this.direction = direction;
+    }
+
+    public Rendition getRendition() {
+        return rendition;
+    }
+
+    public void setRendition(Rendition rendition) {
+        this.rendition = rendition;
+    }
+
+    public String getSource() {
+        return source;
+    }
+
+    public void setSource(String source) {
+        this.source = source;
+    }
+
+    public List<String> getEpubType() {
+        return epubType;
+    }
+
+    public void setEpubType(List<String> epubType) {
+        this.epubType = epubType;
+    }
+
+    public List<String> getRights() {
+        return rights;
+    }
+
+    public void setRights(List<String> rights) {
+        this.rights = rights;
+    }
+
+    public List<Subject> getSubjects() {
+        return subjects;
+    }
+
+    public void setSubjects(List<Subject> subjects) {
+        this.subjects = subjects;
+    }
+
+    public List<MetadataItem> getOtherMetadata() {
+        return otherMetadata;
+    }
+
+    public void setOtherMetadata(List<MetadataItem> otherMetadata) {
+        this.otherMetadata = otherMetadata;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/metadata/MetadataItem.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/metadata/MetadataItem.java
new file mode 100755 (executable)
index 0000000..99035f2
--- /dev/null
@@ -0,0 +1,44 @@
+package org.readium.r2_streamer.model.publication.metadata;
+
+import org.readium.r2_streamer.model.publication.SMIL.SMILParser;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public class MetadataItem implements Serializable {
+    private static final long serialVersionUID = 7526471195622776147L;
+    public String property;
+    public String value;
+    public List<MetadataItem> children;
+
+    public MetadataItem() {
+    }
+
+    public MetadataItem(String property, String value, List<MetadataItem> children) {
+        this.property = property;
+        this.value = value;
+        this.children = children;
+    }
+
+    @Override
+    public String toString() {
+        return "MetadataItem{" +
+                "property='" + property + '\'' +
+                ", value='" + value + '\'' +
+                ", children=" + children +
+                '}';
+    }
+
+    public static String getSMILDuration(List<MetadataItem> otherMetadata, String id) {
+        for (MetadataItem metadataItem : otherMetadata) {
+            if (metadataItem.property.equalsIgnoreCase("#" + id)) {
+                return SMILParser.smilTimeToSeconds(metadataItem.value);
+            }
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/Rendition.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/Rendition.java
new file mode 100755 (executable)
index 0000000..b1a5d83
--- /dev/null
@@ -0,0 +1,78 @@
+package org.readium.r2_streamer.model.publication.rendition;
+
+import java.io.Serializable;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public class Rendition implements Serializable {
+    private static final long serialVersionUID = 7426472295421776147L;
+    public RenditionLayout layout;
+    public RenditionFlow flow;
+    public RenditionOrientation orientation;
+    public RenditionSpread spread;
+    public String viewport;
+
+    public Rendition() {
+    }
+
+    public Rendition(RenditionLayout layout, RenditionFlow flow, RenditionOrientation orientation, RenditionSpread spread, String viewport) {
+        this.layout = layout;
+        this.flow = flow;
+        this.orientation = orientation;
+        this.spread = spread;
+        this.viewport = viewport;
+    }
+
+    @Override
+    public String toString() {
+        return "Rendition{" +
+                "layout=" + layout +
+                ", flow=" + flow +
+                ", orientation=" + orientation +
+                ", spread=" + spread +
+                ", viewport='" + viewport + '\'' +
+                '}';
+    }
+
+    public RenditionLayout getLayout() {
+        return layout;
+    }
+
+    public void setLayout(RenditionLayout layout) {
+        this.layout = layout;
+    }
+
+    public RenditionFlow getFlow() {
+        return flow;
+    }
+
+    public void setFlow(RenditionFlow flow) {
+        this.flow = flow;
+    }
+
+    public RenditionOrientation getOrientation() {
+        return orientation;
+    }
+
+    public void setOrientation(RenditionOrientation orientation) {
+        this.orientation = orientation;
+    }
+
+    public RenditionSpread getSpread() {
+        return spread;
+    }
+
+    public void setSpread(RenditionSpread spread) {
+        this.spread = spread;
+    }
+
+    public String getViewport() {
+        return viewport;
+    }
+
+    public void setViewport(String viewport) {
+        this.viewport = viewport;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionFlow.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionFlow.java
new file mode 100755 (executable)
index 0000000..0257584
--- /dev/null
@@ -0,0 +1,24 @@
+package org.readium.r2_streamer.model.publication.rendition;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public enum RenditionFlow {
+    PAGINATED("paginated"), CONTINUOUS("continuous"), DOCUMENT("document"), FIXED("fixed");
+
+    String value;
+
+    RenditionFlow(String value) {
+        this.value = value;
+    }
+
+    public static RenditionFlow valueOfEnum(String name) {
+        for (RenditionFlow layout : RenditionFlow.values()) {
+            if (layout.value.equals(name)) {
+                return layout;
+            }
+        }
+        throw new IllegalArgumentException(name);
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionLayout.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionLayout.java
new file mode 100755 (executable)
index 0000000..f071a2d
--- /dev/null
@@ -0,0 +1,24 @@
+package org.readium.r2_streamer.model.publication.rendition;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public enum RenditionLayout {
+    REFLOWABLE("reflowable"), PREPAGINATED("pre-paginated");
+
+    String value;
+
+    RenditionLayout(String value) {
+        this.value = value;
+    }
+
+    public static RenditionLayout valueOfEnum(String name) {
+        for (RenditionLayout layout : RenditionLayout.values()) {
+            if (layout.value.equals(name)) {
+                return layout;
+            }
+        }
+        throw new IllegalArgumentException(name);
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionOrientation.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionOrientation.java
new file mode 100755 (executable)
index 0000000..d451617
--- /dev/null
@@ -0,0 +1,24 @@
+package org.readium.r2_streamer.model.publication.rendition;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public enum RenditionOrientation {
+    AUTO("auto"), LANDSCAPE("landscape"), PORTRAIT("portrait");
+
+    String value;
+
+    RenditionOrientation(String value) {
+        this.value = value;
+    }
+
+    public static RenditionOrientation valueOfEnum(String name) {
+        for (RenditionOrientation layout : RenditionOrientation.values()) {
+            if (layout.value.equals(name)) {
+                return layout;
+            }
+        }
+        throw new IllegalArgumentException(name);
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionSpread.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/rendition/RenditionSpread.java
new file mode 100755 (executable)
index 0000000..1cbd597
--- /dev/null
@@ -0,0 +1,24 @@
+package org.readium.r2_streamer.model.publication.rendition;
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public enum RenditionSpread {
+    AUTO("auto"), LANDSCAPE("landscape"), PORTRAIT("portrait"), BOTH("both"), NONE("none");
+
+    String value;
+
+    RenditionSpread(String value) {
+        this.value = value;
+    }
+
+    public static RenditionSpread valueOfEnum(String name) {
+        for (RenditionSpread layout : RenditionSpread.values()) {
+            if (layout.value.equals(name)) {
+                return layout;
+            }
+        }
+        throw new IllegalArgumentException(name);
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/subject/Subject.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/publication/subject/Subject.java
new file mode 100755 (executable)
index 0000000..6bd8f27
--- /dev/null
@@ -0,0 +1,66 @@
+package org.readium.r2_streamer.model.publication.subject;
+
+import java.io.Serializable;
+
+
+/**
+ * Created by Shrikant Badwaik on 25-Jan-17.
+ */
+
+public class Subject implements Serializable{
+    private static final long serialVersionUID = 7526472295622776147L;
+
+    public String name;
+    public String sortAs;
+    public String scheme;
+    public String code;
+
+    public Subject() {
+    }
+
+    @Override
+    public String toString() {
+        return "Subject{" +
+                "name='" + name + '\'' +
+                ", sortAs='" + sortAs + '\'' +
+                ", scheme='" + scheme + '\'' +
+                ", code='" + code + '\'' +
+                '}';
+    }
+
+    public Subject(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getSortAs() {
+        return sortAs;
+    }
+
+    public void setSortAs(String sortAs) {
+        this.sortAs = sortAs;
+    }
+
+    public String getScheme() {
+        return scheme;
+    }
+
+    public void setScheme(String scheme) {
+        this.scheme = scheme;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/searcher/SearchQueryResults.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/searcher/SearchQueryResults.java
new file mode 100755 (executable)
index 0000000..e4c1dfd
--- /dev/null
@@ -0,0 +1,35 @@
+package org.readium.r2_streamer.model.searcher;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Shrikant Badwaik on 10-Mar-17.
+ */
+
+public class SearchQueryResults {
+    private int searchCount;
+    public List<SearchResult> searchResultList;
+
+    public SearchQueryResults() {
+        this.searchResultList = new ArrayList<>();
+    }
+
+    public SearchQueryResults(int searchCount, List<SearchResult> searchResultList) {
+        this.searchCount = searchCount;
+        this.searchResultList = searchResultList;
+    }
+
+    public int getSearchCount() {
+        searchCount = searchResultList.size();
+        return searchCount;
+    }
+
+    public List<SearchResult> getSearchResultList() {
+        return searchResultList;
+    }
+
+    public void setSearchResultList(List<SearchResult> searchResultList) {
+        this.searchResultList = searchResultList;
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/searcher/SearchResult.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/searcher/SearchResult.java
new file mode 100755 (executable)
index 0000000..5358204
--- /dev/null
@@ -0,0 +1,84 @@
+package org.readium.r2_streamer.model.searcher;
+
+/**
+ * Created by Shrikant Badwaik on 17-Feb-17.
+ */
+
+public class SearchResult {
+    private int searchIndex;
+    private String resource;
+    private String title;
+    private String searchQuery;
+    private String matchString;
+    private String textBefore;
+    private String textAfter;
+
+    public SearchResult() {
+    }
+
+    public SearchResult(int searchIndex, String resource, String title, String searchQuery, String matchString, String textBefore, String textAfter) {
+        this.searchIndex = searchIndex;
+        this.resource = resource;
+        this.title = title;
+        this.searchQuery = searchQuery;
+        this.matchString = matchString;
+        this.textBefore = textBefore;
+        this.textAfter = textAfter;
+    }
+
+    public int getSearchIndex() {
+        return searchIndex;
+    }
+
+    public void setSearchIndex(int searchIndex) {
+        this.searchIndex = searchIndex;
+    }
+
+    public String getResource() {
+        return resource;
+    }
+
+    public void setResource(String resource) {
+        this.resource = resource;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getSearchQuery() {
+        return searchQuery;
+    }
+
+    public void setSearchQuery(String searchQuery) {
+        this.searchQuery = searchQuery;
+    }
+
+    public String getMatchString() {
+        return matchString;
+    }
+
+    public void setMatchString(String matchString) {
+        this.matchString = matchString;
+    }
+
+    public String getTextBefore() {
+        return textBefore;
+    }
+
+    public void setTextBefore(String textBefore) {
+        this.textBefore = textBefore;
+    }
+
+    public String getTextAfter() {
+        return textAfter;
+    }
+
+    public void setTextAfter(String textAfter) {
+        this.textAfter = textAfter;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/tableofcontents/TOCLink.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/model/tableofcontents/TOCLink.java
new file mode 100755 (executable)
index 0000000..4e3e4de
--- /dev/null
@@ -0,0 +1,60 @@
+package org.readium.r2_streamer.model.tableofcontents;
+
+import org.readium.r2_streamer.model.publication.link.Link;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Shrikant Badwaik on 27-Feb-17.
+ */
+
+public class TOCLink extends Link implements Serializable{
+    private static final long serialVersionUID = 752647222222776147L;
+    public String sectionTitle;
+    public String playOrder;
+    public List<TOCLink> tocLinks;
+
+    public TOCLink() {
+        tocLinks = new ArrayList<>();
+    }
+
+    public TOCLink(String sectionTitle, String playOrder, ArrayList<TOCLink> navPoints) {
+        this.sectionTitle = sectionTitle;
+        this.playOrder = playOrder;
+        this.tocLinks = navPoints;
+    }
+
+    @Override
+    public String toString() {
+        return "TOCLink{" +
+                "sectionTitle='" + bookTitle + '\'' +
+                ", tocLinks=" + tocLinks +
+                '}';
+    }
+
+    public String getSectionTitle() {
+        return sectionTitle;
+    }
+
+    public void setSectionTitle(String sectionTitle) {
+        this.sectionTitle = sectionTitle;
+    }
+
+    public String getPlayOrder() {
+        return playOrder;
+    }
+
+    public void setPlayOrder(String playOrder) {
+        this.playOrder = playOrder;
+    }
+
+    public List<TOCLink> getTocLinks() {
+        return tocLinks;
+    }
+
+    public void setTocLinks(List<TOCLink> navPoints) {
+        this.tocLinks = navPoints;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/CBZParser.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/CBZParser.java
new file mode 100755 (executable)
index 0000000..9c4e3ec
--- /dev/null
@@ -0,0 +1,56 @@
+package org.readium.r2_streamer.parser;
+
+import org.readium.r2_streamer.model.container.Container;
+import org.readium.r2_streamer.model.publication.EpubPublication;
+import org.readium.r2_streamer.model.publication.link.Link;
+
+/**
+ * Class to handle parsing of the .cbz (Comic book archive)
+ * ref => https://en.wikipedia.org/wiki/Comic_book_archive
+ *
+ * @author gautam chibde on 5/6/17.
+ */
+
+public class CBZParser {
+    private static final String TAG = CBZParser.class.getSimpleName();
+
+    /**
+     * function converts all the images inside the .cbz file into
+     * link and addes them to spine and linkMap
+     *
+     * @param container   contains implementation for getting raw data from file.
+     * @param publication The `Publication` object resulting from the parsing.
+     */
+    public static void parseCBZ(Container container, EpubPublication publication) {
+
+        publication.internalData.put("type", "cbz");
+        // since all the image files are inside zip rootpath is kept empty
+        publication.internalData.put("rootfile", "");
+
+        for (String name : container.listFiles()) {
+            Link link = new Link();
+            link.typeLink = getMediaType(name);
+            link.href = name;
+            // Add the book images to the spine element
+            publication.spines.add(link);
+            // Add to the resource linkMap for ResourceHandler to publish on the server
+            publication.linkMap.put(name, link);
+        }
+    }
+
+    /**
+     * Returns the mimetype depending on the file format
+     *
+     * @param name file name
+     * @return mimetype of the input file
+     */
+    private static String getMediaType(String name) {
+        if (name.contains(".jpg") || name.contains("jpeg")) {
+            return "image/jpeg";
+        } else if (name.contains("png")) {
+            return "image/png";
+        } else {
+            return "";
+        }
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EncryptionDecoder.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EncryptionDecoder.java
new file mode 100755 (executable)
index 0000000..8562f35
--- /dev/null
@@ -0,0 +1,173 @@
+package org.readium.r2_streamer.parser;
+
+import org.readium.r2_streamer.model.publication.Encryption;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Formatter;
+import java.util.HashMap;
+
+/**
+ * Created by gautam chibde on 18/5/17.
+ */
+
+public final class EncryptionDecoder {
+    private final static String TAG = EncryptionDecoder.class.getName();
+
+    private static HashMap<String, Integer> decoders = new HashMap<>();
+
+    static {
+        EncryptionDecoder.decoders.put("http://www.idpf.org/2008/embedding", 1040);
+        EncryptionDecoder.decoders.put("http://ns.adobe.com/pdf/enc#RC", 1024);
+    }
+
+    /**
+     * Decode obfuscated font from a InputStream, if the encryption is known.
+     *
+     * @param identifier The associated publication Identifier.
+     * @param inStream   font input stream
+     * @param encryption {@link Encryption} object contains encryption type
+     * @return The InputStream containing the unencrypted resource.
+     */
+    public static InputStream decode(String identifier, InputStream inStream, Encryption encryption) {
+        if (identifier == null) {
+            System.out.println(TAG + " Couldn't get the publication identifier.");
+            return inStream;
+        }
+
+        if (!decoders.containsKey(encryption.getAlgorithm())) {
+            System.out.println(TAG + " " + encryption.getProfile() + " is encrypted but decoder cant handle it");
+            return inStream;
+        }
+        return decodeFont(identifier, inStream, decoders.get(encryption.getAlgorithm()), encryption.getAlgorithm());
+    }
+
+    /**
+     * Decode the given inputStream first X characters, depending of the obfuscation type.
+     *
+     * @param identifier The associated publication Identifier.
+     * @param inStream   The input stream containing the data of an obfuscated font
+     * @param length     The ObfuscationLength depending of the obfuscation type.
+     * @param algorithm  type of algorithm
+     * @return The Deobfuscated InputStream.
+     */
+    private static InputStream decodeFont(String identifier, InputStream inStream, Integer length, String algorithm) {
+
+        byte[] publicationKey = null;
+        switch (algorithm) {
+            case "http://ns.adobe.com/pdf/enc#RC":
+                publicationKey = getHashKeyAdobe(identifier);
+                break;
+            case "http://www.idpf.org/2008/embedding":
+                publicationKey = getHashKeyIdpf(identifier);
+                break;
+        }
+        return deobfuscate(inStream, publicationKey, length);
+    }
+
+    /**
+     * Receive an obfuscated InputStream and return deabfuscated InputStream
+     *
+     * @param inStream       The input stream containing the data of an obfuscated font
+     * @param publicationKey The publicationKey used to decode the X first characters.
+     * @param length         The number of characters obfuscated at the first of the file.
+     * @return The Deobfuscated InputStream.
+     */
+    private static InputStream deobfuscate(InputStream inStream, byte[] publicationKey, Integer length) {
+        if (publicationKey == null) {
+            return inStream;
+        }
+        try {
+            byte[] bytes = new byte[inStream.available()];
+            int size = inStream.read(bytes);
+            int count = size > length ? length : size;
+            int pubKeyLength = publicationKey.length;
+            int i = 0;
+            while (i < count) {
+                bytes[i] = (byte) (bytes[i] ^ (publicationKey[i % pubKeyLength]));
+                i++;
+            }
+            return new ByteArrayInputStream(bytes);
+        } catch (IOException e) {
+            System.out.println(TAG + ".deobfuscate() " + e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Create an EPUB font obfuscation key from one or more strings according to the rules
+     * defined in the EPUB 3 spec, 4.3 Generating the Obfuscation Key
+     * (http://www.idpf.org/epub/30/spec/epub30-ocf.html#fobfus-keygen)
+     * <p>
+     * Squeezes out any whitespace in each UID and then concatenates the result
+     * using single space characters as the separator.
+     *
+     * @param identifier The string to convert into a key.
+     * @return obfuscation key string
+     */
+    private static byte[] getHashKeyIdpf(String identifier) {
+        try {
+            MessageDigest crypt = MessageDigest.getInstance("SHA-1");
+            crypt.reset();
+            crypt.update(identifier.getBytes("UTF-8"));
+            return hexToBytes(byteToHex(crypt.digest()));
+        } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Generate the Hashkey used to salt the 1024 starting character of the Adobe font files.
+     *
+     * @param pubId The publication Identifier.
+     * @return The key's bytes array.
+     */
+    private static byte[] getHashKeyAdobe(String pubId) {
+        String cleanPubId = pubId.replaceAll("urn:uuid", "");
+        cleanPubId = cleanPubId.replaceAll("-", "");
+        cleanPubId = cleanPubId.replaceAll(":","");
+        return hexToBytes(cleanPubId);
+    }
+
+    /**
+     * Convert hexadecimal String to Bytes (UInt8) array.
+     *
+     * @param hexa The hexadecimal String
+     * @return The key's bytes array.
+     */
+    private static byte[] hexToBytes(String hexa) {
+        char[] hex = hexa.toCharArray();
+        int length = hex.length / 2;
+        byte[] raw = new byte[length];
+        for (int i = 0; i < length; i++) {
+            int high = Character.digit(hex[i * 2], 16);
+            int low = Character.digit(hex[i * 2 + 1], 16);
+            int value = (high << 4) | low;
+            if (value > 127)
+                value -= 256;
+            raw[i] = (byte) value;
+        }
+        return raw;
+    }
+
+    /**
+     * Convert Bytes array to hexadecimal String.
+     *
+     * @param hash bytes array.
+     * @return The hexadecimal String.
+     */
+    private static String byteToHex(byte[] hash) {
+        Formatter formatter = new Formatter();
+        for (byte b : hash) {
+            formatter.format("%02x", b);
+        }
+        String result = formatter.toString();
+        formatter.close();
+        return result;
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EncryptionParser.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EncryptionParser.java
new file mode 100755 (executable)
index 0000000..6d34082
--- /dev/null
@@ -0,0 +1,66 @@
+package org.readium.r2_streamer.parser;
+
+import org.readium.r2_streamer.model.container.Container;
+import org.readium.r2_streamer.model.publication.Encryption;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by gautam chibde on 31/5/17.
+ */
+
+public class EncryptionParser {
+
+    private static final String TAG = EncryptionParser.class.getSimpleName();
+
+    /**
+     * parse file encryption.xml located at META-INF/encryption.xml
+     * <p>
+     * content of the encryption file are saved as {@link Encryption} where path to
+     * encrypted file is saved in {@link Encryption#profile} and
+     * encryption algorithm in {@link Encryption#algorithm}
+     */
+    public static List<Encryption> parseEncryption(Container container) {
+        String containerPath = "META-INF/encryption.xml";
+        try {
+            String containerData = container.rawData(containerPath);
+            Document encryptionDocument = EpubParser.xmlParser(containerData);
+            if (encryptionDocument == null) {
+                throw new EpubParserException("Error while paring META-INF/encryption.xml");
+            }
+            NodeList element = encryptionDocument.getDocumentElement().getElementsByTagName("EncryptedData");
+
+            List<Encryption> encryptions = new ArrayList<>();
+            for (int i = 0; i < element.getLength(); i++) {
+                Encryption encryption = new Encryption();
+                Element algorithmElement = (Element) ((Element) element.item(i)).getElementsByTagName("EncryptionMethod").item(0);
+                Element pathElement = (Element) ((Element) ((Element) element.item(i)).getElementsByTagName("CipherData").item(0)).getElementsByTagName("CipherReference").item(0);
+                if (algorithmElement != null) {
+                    if (algorithmElement.hasAttribute("Algorithm")) {
+                        encryption.setAlgorithm(algorithmElement.getAttribute("Algorithm"));
+                    }
+                }
+                if (pathElement != null) {
+                    if (pathElement.hasAttribute("URI")) {
+                        encryption.setProfile(pathElement.getAttribute("URI"));
+                    }
+                }
+                //TODO properties
+                //TODO LCP
+                encryptions.add(encryption);
+            }
+            return encryptions;
+        } catch (EpubParserException e) {
+            e.printStackTrace();
+            return null;
+        } catch (NullPointerException e) {
+            System.out.println(TAG + " META-INF/encryption.xml not found " + e);
+            return null;
+        }
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EpubParser.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EpubParser.java
new file mode 100755 (executable)
index 0000000..6f2407d
--- /dev/null
@@ -0,0 +1,133 @@
+package org.readium.r2_streamer.parser;
+
+import org.readium.r2_streamer.model.container.Container;
+import org.readium.r2_streamer.model.publication.EpubPublication;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * Created by Shrikant Badwaik on 27-Jan-17.
+ */
+
+public class EpubParser {
+    private final String TAG = "EpubParser";
+
+    private Container container;        //can be either EpubContainer or DirectoryContainer
+    private EpubPublication publication;
+    //private String epubVersion;
+
+    public EpubParser(Container container) {
+        this.container = container;
+        this.publication = new EpubPublication();
+    }
+
+    public EpubPublication parseEpubFile(String filePath) {
+        String rootFile;
+        try {
+            if (filePath.contains(".cbz")) {
+                CBZParser.parseCBZ(container, publication);
+                return publication;
+            }
+            if (isMimeTypeValid()) {
+                rootFile = parseContainer();
+
+                publication.internalData.put("type", "epub");
+                publication.internalData.put("rootfile", rootFile);
+                //Parse OPF file
+                this.publication = OPFParser.parseOpfFile(rootFile, this.publication, container);
+                // Parse Encryption
+                this.publication.encryptions = EncryptionParser.parseEncryption(container);
+                // Parse Media Overlay
+                MediaOverlayParser.parseMediaOverlay(this.publication, container);
+                return publication;
+            }
+        } catch (EpubParserException e) {
+            System.out.println(TAG + " parserEpubFile() error " + e.toString());
+        }
+        return null;
+    }
+
+    private boolean isMimeTypeValid() throws EpubParserException {
+        String mimeTypeData = container.rawData("mimetype");
+
+        if (mimeTypeData.equals("application/epub+zip")) {
+            return true;
+        } else {
+            System.out.println(TAG + "Invalid MIME type: " + mimeTypeData);
+            throw new EpubParserException("Invalid MIME type");
+        }
+    }
+
+    private String parseContainer() throws EpubParserException {
+        String containerPath = "META-INF/container.xml";
+        String containerData = container.rawData(containerPath);
+
+        if (containerData == null) {
+            System.out.println(TAG + " File is missing: " + containerPath);
+            throw new EpubParserException("File is missing");
+        }
+
+        String opfFile = containerXmlParser(containerData);
+        if (opfFile == null) {
+            throw new EpubParserException("Error while parsing");
+        }
+        return opfFile;
+    }
+
+    //@Nullable
+    private String containerXmlParser(String containerData) throws EpubParserException {           //parsing container.xml
+        try {
+            String xml = containerData.replaceAll("[^\\x20-\\x7e]", "").trim();         //in case encoding problem
+
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document document = builder.parse(new InputSource(new StringReader(xml)));
+            document.getDocumentElement().normalize();
+            if (document == null) {
+                throw new EpubParserException("Error while parsing container.xml");
+            }
+
+            Element rootElement = (Element) ((Element) document.getDocumentElement().getElementsByTagName("rootfiles").item(0)).getElementsByTagName("rootfile").item(0);
+            if (rootElement != null) {
+                String opfFile = rootElement.getAttribute("full-path");
+                if (opfFile == null) {
+                    throw new EpubParserException("Missing root file element in container.xml");
+                }
+
+                return opfFile;                    //returns opf file
+            }
+        } catch (ParserConfigurationException | SAXException | IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+
+    //@Nullable
+    public static Document xmlParser(String xmlData) throws EpubParserException {
+        try {
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document document = builder.parse(new InputSource(new StringReader(xmlData)));
+            document.getDocumentElement().normalize();
+            if (document == null) {
+                throw new EpubParserException("Error while parsing xml file");
+            }
+
+            return document;
+        } catch (ParserConfigurationException | SAXException | IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EpubParserException.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/EpubParserException.java
new file mode 100755 (executable)
index 0000000..e138d7a
--- /dev/null
@@ -0,0 +1,11 @@
+package org.readium.r2_streamer.parser;
+
+/**
+ * Created by Shrikant Badwaik on 27-Jan-17.
+ */
+
+public class EpubParserException extends Exception {
+    public EpubParserException(String message) {
+        super(message);
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/MediaOverlayParser.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/MediaOverlayParser.java
new file mode 100755 (executable)
index 0000000..4b7a34e
--- /dev/null
@@ -0,0 +1,200 @@
+package org.readium.r2_streamer.parser;
+
+import org.readium.r2_streamer.model.container.Container;
+import org.readium.r2_streamer.model.publication.EpubPublication;
+import org.readium.r2_streamer.model.publication.SMIL.MediaOverlayNode;
+import org.readium.r2_streamer.model.publication.SMIL.SMILParser;
+import org.readium.r2_streamer.model.publication.link.Link;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.List;
+
+/**
+ * Created by gautam chibde on 31/5/17.
+ */
+
+public class MediaOverlayParser {
+
+    /**
+     * Looks for the link with type: application/smil+xml and parsed the
+     * data as media-overlay
+     * also adds link for media-overlay for specific file
+     *
+     * @param publication The `Publication` object resulting from the parsing.
+     * @param container   contains implementation for getting raw data from file
+     * @throws EpubParserException if file is invalid for not found
+     */
+    public static void parseMediaOverlay(EpubPublication publication, Container container) throws EpubParserException {
+        for (String key : publication.linkMap.keySet()) {
+            if (publication.linkMap.get(key).typeLink.equalsIgnoreCase("application/smil+xml")) {
+                Link link = publication.linkMap.get(key);
+                String smip = container.rawData(link.getHref());
+                if (smip == null) return; // maybe file is invalid
+
+                Document document = EpubParser.xmlParser(smip);
+
+                if (document == null)
+                    throw new EpubParserException("Error while parsing file " + link.href);
+
+                Element body = (Element) document.getDocumentElement().getElementsByTagName("body").item(0);
+
+                MediaOverlayNode node = new MediaOverlayNode();
+                node.role.add("section");
+
+                if (body.hasAttribute("epub:textref"))
+                    node.text = body.getAttribute("epub:textref");
+
+                parseParameters(body, node, link.href);
+                parseSequences(body, node, publication, link.href);
+
+                // TODO
+                // Body attribute epub:textref is optional
+                // ref https://www.idpf.org/epub/30/spec/epub30-mediaoverlays.html#sec-smil-body-elem
+                // need to handle <seq> parsing in an alternate way
+
+                if (node.text != null) {
+                    String baseHref = node.text.split("#")[0];
+                    int position = getPosition(publication.spines, baseHref);
+
+                    if (position != -1) {
+                        addMediaOverlayToSpine(publication, node, position);
+                    }
+                } else {
+                    for (MediaOverlayNode node1 : node.children) {
+                        int position = getPosition(publication.spines, node1.text);
+                        if (position != -1) {
+                            addMediaOverlayToSpine(publication, node1, position);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * [RECURSIVE]
+     * <p>
+     * Parse the <seq> elements at the current XML level. It will recursively
+     * parse their children's <par> and <seq>
+     *
+     * @param body input element with seq tag
+     * @param node contains parsed <seq><par></par></seq> elements
+     * @param href path of SMIL file
+     */
+    private static void parseSequences(Element body, MediaOverlayNode node, EpubPublication publication, String href) throws StackOverflowError {
+        if (body == null || !body.hasChildNodes()) {
+            return;
+        }
+        for (Node n = body.getFirstChild(); n != null; n = n.getNextSibling()) {
+            if (n.getNodeType() == Node.ELEMENT_NODE) {
+                Element e = (Element) n;
+                if (e.getTagName().equalsIgnoreCase("seq")) {
+                    MediaOverlayNode mediaOverlayNode = new MediaOverlayNode();
+
+                    if (e.hasAttribute("epub:textref"))
+                        mediaOverlayNode.text = e.getAttribute("epub:textref");
+
+                    mediaOverlayNode.role.add("section");
+
+                    // child <par> elements in seq
+                    parseParameters(e, mediaOverlayNode, href);
+                    node.children.add(mediaOverlayNode);
+                    // recur to parse child node elements
+                    parseSequences(e, mediaOverlayNode, publication, href);
+
+                    if (node.text == null) return;
+
+                    // Not clear about the IRI reference, epub:textref in seq may not have [ "#" ifragment ]
+                    // ref:- https://www.idpf.org/epub/30/spec/epub30-mediaoverlays.html#sec-smil-seq-elem
+                    // TODO is it req? code ref from https://github.com/readium/r2-streamer-swift/blob/feature/media-overlay/Sources/parser/SMILParser.swift
+                    // can be done with contains?
+
+                    String baseHrefParent = node.text;
+                    if (node.text.contains("#")) {
+                        baseHrefParent = node.text.split("#")[0];
+                    }
+                    if (mediaOverlayNode.text.contains("#")) {
+                        String baseHref = mediaOverlayNode.text.split("#")[0];
+
+                        if (!baseHref.equals(baseHrefParent)) {
+                            int position = getPosition(publication.spines, baseHref);
+
+                            if (position != -1)
+                                addMediaOverlayToSpine(publication, mediaOverlayNode, position);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Parse the <par> elements at the current XML element level.
+     *
+     * @param body input element with seq tag
+     * @param node contains parsed <par></par> elements
+     */
+    private static void parseParameters(Element body, MediaOverlayNode node, String href) {
+        NodeList par = body.getElementsByTagName("par");
+        if (par.getLength() == 0) {
+            return;
+        }
+        // For each <par> in the current scope.
+        for (Node n = body.getFirstChild(); n != null; n = n.getNextSibling()) {
+            if (n.getNodeType() == Node.ELEMENT_NODE) {
+                Element e = (Element) n;
+                if (e.getTagName().equalsIgnoreCase("par")) {
+                    MediaOverlayNode mediaOverlayNode = new MediaOverlayNode();
+                    Element text = (Element) e.getElementsByTagName("text").item(0);
+                    Element audio = (Element) e.getElementsByTagName("audio").item(0);
+
+                    if (text != null) mediaOverlayNode.text = text.getAttribute("src");
+                    if (audio != null) {
+                        mediaOverlayNode.audio = SMILParser.parseAudio(audio, href);
+                    }
+                    node.children.add(mediaOverlayNode);
+                }
+            }
+        }
+    }
+
+    /**
+     * Add parsed media-overlay object to corresponding spine item
+     *
+     * @param publication publication object
+     * @param node        parsed media overlay node
+     * @param position    position on spine item in publication
+     */
+    private static void addMediaOverlayToSpine(EpubPublication publication, MediaOverlayNode node, int position) {
+        publication.spines.get(position).mediaOverlay.mediaOverlayNodes.add(node);
+        publication.spines.get(position).properties.add("media-overlay?resource=" + publication.spines.get(position).href);
+
+        publication.links.add(new Link(
+                "port/media-overlay?resource=" + publication.spines.get(position).href, //replace the port with proper url in EpubServer#addLinks
+                "media-overlay",
+                "application/vnd.readium.mo+json"));
+    }
+
+    /**
+     * returns position of the spine whose href equals baseHref
+     *
+     * @param spines   spine list in publication
+     * @param baseHref name of the file which corresponding to media-overlay
+     * @return returns position of the spine item
+     */
+    private static int getPosition(List<Link> spines, String baseHref) {
+        for (Link link : spines) {
+            int offset = link.href.indexOf("/", 0);
+            int startIndex = link.href.indexOf("/", offset + 1);
+            String path = link.href.substring(startIndex + 1);
+            if (baseHref.contains(path)) {
+                return spines.indexOf(link);
+            }
+        }
+        return -1;
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/NCXParser.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/NCXParser.java
new file mode 100755 (executable)
index 0000000..b9a21dd
--- /dev/null
@@ -0,0 +1,105 @@
+package org.readium.r2_streamer.parser;
+
+import org.readium.r2_streamer.model.container.Container;
+import org.readium.r2_streamer.model.publication.EpubPublication;
+import org.readium.r2_streamer.model.tableofcontents.TOCLink;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by gautam chibde on 31/5/17.
+ */
+
+public class NCXParser {
+
+    private static final String TAG = NCXParser.class.getSimpleName();
+
+    public static void parseNCXFile(String ncxFile, Container container, EpubPublication publication, String rootPath) throws EpubParserException {
+        String ncxData = container.rawData(ncxFile);
+        if (ncxData == null) {
+            return; // File is missing
+        }
+        Document document = EpubParser.xmlParser(ncxData);
+        if (document == null) {
+            throw new EpubParserException("Error while parsing");
+        }
+
+        Element navMapElement = (Element) document.getElementsByTagName("navMap").item(0);
+        // Parse table of contents (toc) from ncx file
+        if (navMapElement != null) {
+            publication.tableOfContents = nodeArray(navMapElement, "navPoint", rootPath);
+        }
+
+        Element pageList = (Element) document.getElementsByTagName("pageList").item(0);
+        // Parse page list if exists from ncx file
+        if (pageList != null) {
+            publication.pageList = nodeArray(pageList, "pageTarget", rootPath);
+        }
+    }
+
+    /**
+     * Generate an array of {@link TOCLink} elements representation of the XML
+     * structure in the ncx file. Each of them possibly having children.
+     *
+     * @param elements NCX DOM element object
+     * @param type     The sub elements names (e.g. 'navPoint' for 'navMap',
+     *                 'pageTarget' for 'pageList'.
+     * @return The Object representation of the data contained in the given NCX XML element.
+     */
+    private static List<TOCLink> nodeArray(Element elements, String type, String rootPath) {
+        // The "to be returned" node array.
+        List<TOCLink> newNodeArray = new ArrayList<>();
+
+        // Find the elements of `type` in the XML element.
+        for (Node n = elements.getFirstChild(); n != null; n = n.getNextSibling()) {
+            if (n.getNodeType() == Node.ELEMENT_NODE) {
+                Element e = (Element) n;
+                if (e.getTagName().equalsIgnoreCase(type)) {
+                    newNodeArray.add(node(e, type, rootPath));
+                }
+            }
+        }
+        return newNodeArray;
+    }
+
+    /**
+     * [RECURSIVE]
+     * Create a node link from the specified type element.
+     * recur if there are child elements
+     *
+     * @param element the DOM NCX file elemet
+     * @param type    The sub elements names (e.g. 'navPoint' for 'navMap',
+     *                'pageTarget' for 'pageList'.
+     * @return The generated node {@link TOCLink}.
+     */
+    private static TOCLink node(Element element, String type, String rootPath) {
+        TOCLink newNode = new TOCLink();
+
+        Element content = (Element) element.getElementsByTagName("content").item(0);
+        Element navLabel = (Element) element.getElementsByTagName("navLabel").item(0);
+        if (content != null) {
+            newNode.href = rootPath + content.getAttribute("src");
+        }
+        if (navLabel != null) {
+            Element text = (Element) navLabel.getElementsByTagName("text").item(0);
+            if (text != null) {
+                newNode.bookTitle = text.getTextContent();
+            }
+        }
+
+        for (Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) {
+            if (n.getNodeType() == Node.ELEMENT_NODE) {
+                Element e = (Element) n;
+                if (e.getTagName().equalsIgnoreCase(type)) {
+                    newNode.tocLinks.add(node(e, type, rootPath));
+                }
+            }
+        }
+        return newNode;
+    }
+}
diff --git a/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/OPFParser.java b/Android/r2-streamer/r2-parser/src/main/java/org/readium/r2_streamer/parser/OPFParser.java
new file mode 100755 (executable)
index 0000000..1b1ceb9
--- /dev/null
@@ -0,0 +1,404 @@
+package org.readium.r2_streamer.parser;
+
+import org.readium.r2_streamer.model.container.Container;
+import org.readium.r2_streamer.model.publication.EpubPublication;
+import org.readium.r2_streamer.model.publication.contributor.Contributor;
+import org.readium.r2_streamer.model.publication.link.Link;
+import org.readium.r2_streamer.model.publication.metadata.MetaData;
+import org.readium.r2_streamer.model.publication.metadata.MetadataItem;
+import org.readium.r2_streamer.model.publication.rendition.RenditionFlow;
+import org.readium.r2_streamer.model.publication.rendition.RenditionLayout;
+import org.readium.r2_streamer.model.publication.rendition.RenditionOrientation;
+import org.readium.r2_streamer.model.publication.rendition.RenditionSpread;
+import org.readium.r2_streamer.model.publication.subject.Subject;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.NodeList;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by gautam chibde on 31/5/17.
+ */
+
+public class OPFParser {
+
+    private static final String TAG = OPFParser.class.getSimpleName();
+
+    public static EpubPublication parseOpfFile(String rootFile, EpubPublication publication, Container container) throws EpubParserException {
+        String opfData = container.rawData(rootFile);
+        if (opfData == null) {
+            System.out.println(TAG + "File is missing: " + rootFile);
+            throw new EpubParserException("File is missing");
+        }
+
+        Document document = EpubParser.xmlParser(opfData);
+        if (document == null) {
+            throw new EpubParserException("Error while parsing");
+        }
+
+        MetaData metaData = new MetaData();
+
+        //title
+        metaData.title = parseMainTitle(document);
+
+        //identifier
+        metaData.identifier = parseUniqueIdentifier(document);
+
+        //description
+        Element descriptionElement = (Element) ((Element) document.getDocumentElement().getElementsByTagName("metadata").item(0)).getElementsByTagName("dc:description").item(0);
+        if (descriptionElement != null) {
+            metaData.description = descriptionElement.getTextContent();
+        }
+
+        //modified date
+        Element dateElement = (Element) ((Element) document.getDocumentElement().getElementsByTagName("metadata").item(0)).getElementsByTagName("dc:date").item(0);
+        if (dateElement != null) {
+            try {
+                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+                Date modifiedDate = dateFormat.parse(dateElement.getTextContent());
+                metaData.modified = modifiedDate;
+            } catch (ParseException e) {
+                e.printStackTrace();
+            }
+        }
+
+        //subject
+        NodeList subjectNodeList = document.getElementsByTagName("dc:subject");
+        if (subjectNodeList != null) {
+            for (int i = 0; i < subjectNodeList.getLength(); i++) {
+                Element subjectElement = (Element) subjectNodeList.item(i);
+                metaData.subjects.add(new Subject(subjectElement.getTextContent()));
+            }
+        }
+
+        //language
+        NodeList languageNodeList = document.getElementsByTagName("dc:language");
+        if (languageNodeList != null) {
+            for (int i = 0; i < languageNodeList.getLength(); i++) {
+                Element languageElement = (Element) languageNodeList.item(i);
+                metaData.languages.add(languageElement.getTextContent());
+            }
+        }
+
+        //rights
+        NodeList rightNodeList = document.getElementsByTagName("dc:rights");
+        if (rightNodeList != null) {
+            for (int i = 0; i < rightNodeList.getLength(); i++) {
+                Element rightElement = (Element) rightNodeList.item(i);
+                metaData.rights.add(rightElement.getTextContent());
+            }
+        }
+
+        //publisher
+        NodeList publisherNodeList = document.getElementsByTagName("dc:publisher");
+        if (publisherNodeList != null) {
+            for (int i = 0; i < publisherNodeList.getLength(); i++) {
+                Element publisherElement = (Element) publisherNodeList.item(i);
+                metaData.publishers.add(new Contributor(publisherElement.getTextContent()));
+            }
+        }
+
+        //creator
+        NodeList authorNodeList = document.getElementsByTagName("dc:creator");
+        if (authorNodeList != null) {
+            for (int i = 0; i < authorNodeList.getLength(); i++) {
+                Element authorElement = (Element) authorNodeList.item(i);
+                parseContributor(authorElement, document, metaData);
+            }
+        }
+
+        //contributor
+        NodeList contributorNodeList = document.getElementsByTagName("dc:contributor");
+        if (contributorNodeList != null) {
+            for (int i = 0; i < contributorNodeList.getLength(); i++) {
+                Element contributorElement = (Element) contributorNodeList.item(i);
+                parseContributor(contributorElement, document, metaData);
+            }
+        }
+
+        //rendition property
+        NodeList metaNodeList = document.getElementsByTagName("meta");
+        if (metaNodeList != null) {
+            for (int i = 0; i < metaNodeList.getLength(); i++) {
+                Element metaElement = (Element) metaNodeList.item(i);
+                if (metaElement.getAttribute("property").equals("rendition:layout")) {
+                    metaData.rendition.layout = RenditionLayout.valueOfEnum(metaElement.getTextContent());
+                }
+
+                if (metaElement.getAttribute("property").equals("rendition:flow")) {
+                    metaData.rendition.flow = RenditionFlow.valueOfEnum(metaElement.getTextContent());
+                }
+
+                if (metaElement.getAttribute("property").equals("rendition:orientation")) {
+                    metaData.rendition.orientation = RenditionOrientation.valueOfEnum(metaElement.getTextContent());
+                }
+
+                if (metaElement.getAttribute("property").equals("rendition:spread")) {
+                    metaData.rendition.spread = RenditionSpread.valueOfEnum(metaElement.getTextContent());
+                }
+
+                if (metaElement.getAttribute("property").equals("rendition:viewport")) {
+                    metaData.rendition.viewport = metaElement.getTextContent();
+                }
+                if (metaElement.getAttribute("property").equals("media:duration")) {
+                    MetadataItem metadataItem = new MetadataItem();
+                    metadataItem.property = metaElement.getAttribute("refines");
+                    metadataItem.value = metaElement.getTextContent();
+                    metaData.getOtherMetadata().add(metadataItem);
+                }
+            }
+        }
+
+        Element spineElement = (Element) document.getElementsByTagName("spine").item(0);
+        if (spineElement != null) {
+            metaData.direction = spineElement.getAttribute("page-progression-direction");
+        }
+
+        publication.metadata = metaData;
+
+        //cover
+        String coverId = null;
+        if (metaNodeList != null) {
+            for (int i = 0; i < metaNodeList.getLength(); i++) {
+                Element metaElement = (Element) metaNodeList.item(i);
+                if (metaElement.getAttribute("name").equals("cover")) {
+                    coverId = metaElement.getAttribute("content");
+                }
+            }
+        }
+        parseSpineAndResourcesAndGuide(document, publication, coverId, rootFile, container);
+        return publication;
+    }
+
+    //@Nullable
+    private static String parseMainTitle(Document document) {
+        Element titleElement;
+        NodeList titleNodes = document.getElementsByTagName("dc:title");
+        if (titleNodes != null) {
+            if (titleNodes.getLength() > 1) {
+                for (int i = 0; i < titleNodes.getLength(); i++) {
+                    titleElement = (Element) titleNodes.item(i);
+                    String titleId = titleElement.getAttribute("id");
+                    NodeList metaNodes = document.getElementsByTagName("meta");
+                    if (metaNodes != null) {
+                        for (int j = 0; j < metaNodes.getLength(); j++) {
+                            Element metaElement = (Element) metaNodes.item(j);
+                            if (metaElement.getAttribute("property").equals("title-type")) {
+                                if (metaElement.getAttribute("refines").equals("#" + titleId)) {
+                                    if (metaElement.getTextContent().equals("main")) {
+                                        return titleElement.getTextContent();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            } else {
+                titleElement = (Element) titleNodes.item(0);
+                return titleElement.getTextContent();
+            }
+        }
+        return null;
+    }
+
+    //@Nullable
+    private static String parseUniqueIdentifier(Document document) {
+        Element identifierElement;
+        NodeList identifierNodes = document.getElementsByTagName("dc:identifier");
+        if (identifierNodes != null) {
+            if (identifierNodes.getLength() > 1) {
+                for (int i = 0; i < identifierNodes.getLength(); i++) {
+                    identifierElement = (Element) identifierNodes.item(i);
+                    String uniqueId = identifierElement.getAttribute("unique-identifier");
+                    if (identifierElement.getAttribute("id").equals(uniqueId)) {
+                        return identifierElement.getTextContent();
+                    }
+                }
+            } else {
+                identifierElement = (Element) identifierNodes.item(0);
+                return identifierElement.getTextContent();
+            }
+        }
+        return null;
+    }
+
+    private static void parseContributor(Element element, Document document, MetaData metaData) {
+        Contributor contributor = createContributorFromElement(element, document);
+        if (contributor != null) {
+            String role = contributor.getRole();
+            if (role != null) {
+                switch (role) {
+                    case "aut":
+                        metaData.creators.add(contributor);
+                        break;
+                    case "trl":
+                        metaData.translators.add(contributor);
+                        break;
+                    case "art":
+                        metaData.artists.add(contributor);
+                        break;
+                    case "edt":
+                        metaData.editors.add(contributor);
+                        break;
+                    case "ill":
+                        metaData.illustrators.add(contributor);
+                        break;
+                    case "clr":
+                        metaData.colorists.add(contributor);
+                        break;
+                    case "nrt":
+                        metaData.narrators.add(contributor);
+                        break;
+                    case "pbl":
+                        metaData.publishers.add(contributor);
+                        break;
+                    default:
+                        metaData.contributors.add(contributor);
+                        break;
+                }
+            } else {
+                if (element.getTagName().equals("dc:creator")) {
+                    metaData.creators.add(contributor);
+                } else {
+                    metaData.contributors.add(contributor);
+                }
+            }
+        }
+    }
+
+    //@Nullable
+    private static Contributor createContributorFromElement(Element element, Document document) {
+        Contributor contributor = new Contributor(element.getTextContent());
+        if (contributor != null) {
+            if (element.hasAttribute("opf:role")) {
+                String role = element.getAttribute("opf:role");
+                if (role != null) {
+                    contributor.role = role;
+                }
+            }
+            if (element.hasAttribute("opf:file-as")) {
+                String sortAs = element.getAttribute("opf:file-as");
+                if (sortAs != null) {
+                    contributor.sortAs = sortAs;
+                }
+            }
+            if (element.hasAttribute("id")) {
+                String identifier = element.getAttribute("id");
+                if (identifier != null) {
+                    NodeList metas = document.getElementsByTagName("meta");
+                    if (metas != null) {
+                        for (int i = 0; i < metas.getLength(); i++) {
+                            Element metaElement = (Element) metas.item(i);
+                            if (metaElement.getAttribute("property").equals("role")) {
+                                if (metaElement.getAttribute("refines").equals("#" + identifier)) {
+                                    contributor.role = metaElement.getTextContent();
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return contributor;
+        }
+        return null;
+    }
+
+    private static void parseSpineAndResourcesAndGuide(Document document, EpubPublication publication, String coverId, String rootFile, Container container) throws EpubParserException {
+        int startIndex = 0;
+        int endIndex = rootFile.indexOf("/");
+        System.out.println(TAG + " rootFile:= " + rootFile);
+        String packageName = "";
+        if (endIndex != -1) {
+            packageName = rootFile.substring(startIndex, endIndex) + "/";
+        }
+        Map<String, Link> manifestLinks = new HashMap<>();
+
+        NodeList itemNodes = document.getElementsByTagName("item");
+        if (itemNodes != null) {
+            for (int i = 0; i < itemNodes.getLength(); i++) {
+                Element itemElement = (Element) itemNodes.item(i);
+
+                Link link = new Link();
+                NamedNodeMap nodeMap = itemElement.getAttributes();
+                for (int j = 0; j < nodeMap.getLength(); j++) {
+                    Attr attr = (Attr) nodeMap.item(j);
+                    switch (attr.getNodeName()) {
+                        case "href":
+                            link.href = packageName + attr.getNodeValue();
+                            break;
+                        case "media-type":
+                            link.typeLink = attr.getNodeValue();
+                            if (link.typeLink.equalsIgnoreCase("application/smil+xml")) {
+                                link.duration = MetadataItem.getSMILDuration(publication.metadata.getOtherMetadata(), link.id);
+                            }
+                            break;
+                        case "properties":
+                            if (attr.getNodeValue().equals("nav")) {
+                                link.rel.add("contents");
+                            } else if (attr.getNodeValue().equals("cover-image")) {
+                                link.rel.add("cover");
+                            } else if (!attr.getNodeValue().equals("nav") && !attr.getNodeValue().equals("cover-image")) {
+                                link.properties.add(attr.getNodeValue());
+                            }
+                            break;
+                        case "media-overlay":
+                            link.properties.add("media-overlay");
+                            link.properties.add("resource:" + attr.getNodeValue());
+                    }
+                }
+
+                String id = itemElement.getAttribute("id");
+                String href = itemElement.getAttribute("href");
+                if (href != null && href.contains("ncx")) {
+                    NCXParser.parseNCXFile(link.getHref(), container, publication, packageName);
+                }
+                link.setId(id);
+
+                if (id.equals(coverId)) {
+
+                    publication.coverLink = new Link();
+                    publication.coverLink.rel.add("cover");
+                    publication.coverLink.setId(id);
+                    publication.coverLink.setHref(link.getHref());
+                    publication.coverLink.setTypeLink(link.getTypeLink());
+                    publication.coverLink.setProperties(link.getProperties());
+                }
+                publication.linkMap.put(link.href, link);
+                manifestLinks.put(id, link);
+            }
+        }
+
+        NodeList itemRefNodes = document.getElementsByTagName("itemref");
+        if (itemRefNodes != null) {
+            for (int i = 0; i < itemRefNodes.getLength(); i++) {
+                Element itemRefElement = (Element) itemRefNodes.item(i);
+                String id = itemRefElement.getAttribute("idref");
+                if (manifestLinks.containsKey(id)) {
+                    publication.spines.add(manifestLinks.get(id));
+                    manifestLinks.remove(id);
+                }
+            }
+        }
+        publication.resources.addAll(manifestLinks.values());
+
+        NodeList referenceNodes = document.getElementsByTagName("reference");
+        if (referenceNodes != null) {
+            for (int i = 0; i < referenceNodes.getLength(); i++) {
+                Element referenceElement = (Element) referenceNodes.item(i);
+                Link link = new Link();
+                link.setType(referenceElement.getAttribute("type"));
+                link.setChapterTitle(referenceElement.getAttribute("title"));
+                link.setHref(referenceElement.getAttribute("href"));
+                publication.guides.add(link);
+            }
+        }
+    }
+}
diff --git a/Android/r2-streamer/r2-server/.gitignore b/Android/r2-streamer/r2-server/.gitignore
new file mode 100755 (executable)
index 0000000..796b96d
--- /dev/null
@@ -0,0 +1 @@
+/build
diff --git a/Android/r2-streamer/r2-server/build.gradle b/Android/r2-streamer/r2-server/build.gradle
new file mode 100755 (executable)
index 0000000..fa10f4d
--- /dev/null
@@ -0,0 +1,39 @@
+apply plugin: 'java'
+
+ext {
+    bintrayRepo = 'maven'
+    bintrayName = 'readium'
+
+    publishedGroupId = 'org.readium'
+    libraryName = 'r2-server'
+    artifact = 'r2-server'
+
+    libraryDescription = 'Library takes an data from fetcher as an input and exposes in HTTP'
+
+    siteUrl = 'https://github.com/readium/r2-streamer-java'
+    gitUrl = 'https://github.com/readium/r2-streamer-java.git'
+
+    libraryVersion = '0.1.0'
+
+    developerId = 'mobisystech'
+    developerName = 'CodeToArt'
+    developerEmail = 'mahavir@codetoart.com'
+
+    licenseName = 'FreeBSD License'
+    licenseUrl = 'https://en.wikipedia.org/wiki/FreeBSD_Documentation_License#License'
+    allLicenses = ["FreeBSD"]
+}
+
+dependencies {
+    compile fileTree(include: ['*.jar'], dir: 'libs')
+    compile 'org.nanohttpd:nanohttpd:2.3.1'
+    compile 'org.nanohttpd:nanohttpd-nanolets:2.3.1'
+    implementation project(':r2-streamer:r2-fetcher')
+    implementation project(':r2-streamer:r2-parser')
+}
+
+sourceCompatibility = "1.7"
+targetCompatibility = "1.7"
+
+apply from: '../bintray/installv1.gradle'
+apply from: '../bintray/bintrayv1.gradle'
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/EpubServer.java b/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/EpubServer.java
new file mode 100755 (executable)
index 0000000..88490d6
--- /dev/null
@@ -0,0 +1,92 @@
+package org.readium.r2_streamer.server;
+
+import org.readium.r2_streamer.fetcher.EpubFetcher;
+import org.readium.r2_streamer.fetcher.EpubFetcherException;
+import org.readium.r2_streamer.model.container.Container;
+import org.readium.r2_streamer.model.publication.EpubPublication;
+import org.readium.r2_streamer.model.publication.link.Link;
+import org.readium.r2_streamer.parser.EpubParser;
+import org.readium.r2_streamer.server.handler.ManifestHandler;
+import org.readium.r2_streamer.server.handler.MediaOverlayHandler;
+import org.readium.r2_streamer.server.handler.ResourceHandler;
+import org.readium.r2_streamer.server.handler.SearchQueryHandler;
+
+import fi.iki.elonen.router.RouterNanoHTTPD;
+
+/**
+ * Created by Shrikant Badwaik on 20-Jan-17.
+ */
+
+public class EpubServer extends RouterNanoHTTPD {
+    private static final String SEARCH_QUERY_HANDLE = "/search";
+    private static final String MANIFEST_HANDLE = "/manifest";
+    private static final String MANIFEST_ITEM_HANDLE = "/(.*)";
+    private static final String MEDIA_OVERLAY_HANDLE = "/media-overlay";
+    private boolean containsMediaOverlay = false;
+
+    public EpubServer(int portNo) {
+        super(portNo);
+    }
+
+
+    /**
+     * Creates local server routes for manifest,search and media-overlay
+     *
+     * @param container contains implementation for getting raw data from file
+     * @param filePath  path to the epub/cbz file
+     */
+    public void addEpub(Container container, String filePath) {
+        try {
+            EpubPublication publication = parse(container, filePath);
+
+            addLinks(publication, filePath);
+
+            EpubFetcher fetcher = new EpubFetcher(container, publication);
+            if (containsMediaOverlay) {
+                addRoute(filePath + MEDIA_OVERLAY_HANDLE, MediaOverlayHandler.class, fetcher);
+            }
+            //addRoute(filePath + SPINE_HANDLE, EpubHandler.class, fetcher);
+            //addRoute(filePath + TOC_HANDLE, EpubHandler.class, fetcher);
+            addRoute(filePath + MANIFEST_HANDLE, ManifestHandler.class, fetcher);
+            addRoute(filePath + SEARCH_QUERY_HANDLE, SearchQueryHandler.class, fetcher);
+            addRoute(filePath + MANIFEST_ITEM_HANDLE, ResourceHandler.class, fetcher);
+        } catch (EpubFetcherException e) {
+            System.out.println("EpubServer" + " EpubFetcherException: " + e);
+        }
+    }
+
+    /**
+     * Adds links to the publication
+     * <p>
+     * ref=> https://github.org/readium/webpub-manifest#links
+     *
+     * @param publication publication with parsed OPF data
+     * @param filePath
+     */
+    private void addLinks(EpubPublication publication, String filePath) {
+        containsMediaOverlay = false;
+        for (Link link : publication.links) {
+            if (link.rel.contains("media-overlay")) {
+                containsMediaOverlay = true;
+                link.href = link.href.replace("port", "localhost:" + getListeningPort() + filePath);
+            }
+        }
+
+        // A manifest must contain at least one link using the self relationship.
+        // This link must point to the canonical location of the manifest using an absolute URI:
+        publication.links.add(new Link(
+                "localhost:" + getListeningPort() + filePath + MANIFEST_HANDLE,
+                "self",
+                "application/webpub+json"));
+
+        publication.links.add(new Link(
+                "localhost:" + getListeningPort() + filePath + SEARCH_QUERY_HANDLE,
+                "search",
+                "text/html"));
+    }
+
+    private EpubPublication parse(Container container, String filePath) {
+        EpubParser parser = new EpubParser(container);
+        return parser.parseEpubFile(filePath);
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/EpubServerSingleton.java b/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/EpubServerSingleton.java
new file mode 100755 (executable)
index 0000000..dafdfca
--- /dev/null
@@ -0,0 +1,27 @@
+package org.readium.r2_streamer.server;
+
+/**
+ * Created by Shrikant Badwaik on 06-Mar-17.
+ */
+
+public class EpubServerSingleton {
+    private static EpubServer epubServerInstance;
+
+    public static EpubServer getEpubServerInstance() {
+        if (epubServerInstance == null) {
+            epubServerInstance = new EpubServer(8080);
+        }
+        return epubServerInstance;
+    }
+
+    public static EpubServer getEpubServerInstance(int portNumber) {
+        if (epubServerInstance == null) {
+            epubServerInstance = new EpubServer(portNumber);
+        }
+        return epubServerInstance;
+    }
+
+    public static void resetServerInstance() {
+        epubServerInstance = null;
+    }
+}
diff --git a/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/ResponseStatus.java b/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/ResponseStatus.java
new file mode 100755 (executable)
index 0000000..bdea3c7
--- /dev/null
@@ -0,0 +1,10 @@
+package org.readium.r2_streamer.server;
+
+/**
+ * Created by Shrikant Badwaik on 06-Feb-17.
+ */
+
+public class ResponseStatus {
+    public static final String SUCCESS_RESPONSE = "{\"success\":true}";
+    public static final String FAILURE_RESPONSE = "{\"success\":false}";
+}
diff --git a/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/ManifestHandler.java b/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/ManifestHandler.java
new file mode 100755 (executable)
index 0000000..71860a0
--- /dev/null
@@ -0,0 +1,56 @@
+package org.readium.r2_streamer.server.handler;
+
+import org.readium.r2_streamer.fetcher.EpubFetcher;
+import org.readium.r2_streamer.server.ResponseStatus;
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.util.Map;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.router.RouterNanoHTTPD;
+
+/**
+ * Created by mahavir on 3/2/17.
+ */
+
+public class ManifestHandler extends RouterNanoHTTPD.DefaultHandler {
+    private static final String TAG = "ManifestHandler";
+
+    @Override
+    public String getMimeType() {
+        return "application/webpub+json";
+    }
+
+    @Override
+    public String getText() {
+        return ResponseStatus.FAILURE_RESPONSE;
+    }
+
+    @Override
+    public NanoHTTPD.Response.IStatus getStatus() {
+        return NanoHTTPD.Response.Status.OK;
+    }
+
+    @Override
+    public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map<String, String> urlParams, NanoHTTPD.IHTTPSession session) {
+        try {
+
+            EpubFetcher fetcher = uriResource.initParameter(EpubFetcher.class);
+
+            ObjectMapper objectMapper = new ObjectMapper();
+            String json = objectMapper.writeValueAsString(fetcher.publication);
+
+            return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), json);
+
+        } catch (JsonGenerationException | JsonMappingException e) {
+            System.out.println(TAG + " JsonGenerationException | JsonMappingException " + e.toString());
+            return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, getMimeType(), ResponseStatus.FAILURE_RESPONSE);
+        } catch (IOException e) {
+            System.out.println(TAG + " IOException " + e.toString());
+            return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, getMimeType(), ResponseStatus.FAILURE_RESPONSE);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/MediaOverlayHandler.java b/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/MediaOverlayHandler.java
new file mode 100755 (executable)
index 0000000..0f21d4a
--- /dev/null
@@ -0,0 +1,66 @@
+package org.readium.r2_streamer.server.handler;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.readium.r2_streamer.fetcher.EpubFetcher;
+import org.readium.r2_streamer.model.publication.SMIL.MediaOverlayNode;
+import org.readium.r2_streamer.model.publication.SMIL.MediaOverlays;
+import org.readium.r2_streamer.model.publication.link.Link;
+import org.readium.r2_streamer.server.ResponseStatus;
+
+import java.util.List;
+import java.util.Map;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.router.RouterNanoHTTPD;
+
+/**
+ * Created by gautam chibde on 25/5/17.
+ */
+
+public class MediaOverlayHandler extends RouterNanoHTTPD.DefaultHandler {
+    public static final String TAG = MediaOverlayNode.class.getSimpleName();
+
+    @Override
+    public String getText() {
+        return ResponseStatus.FAILURE_RESPONSE;
+    }
+
+    @Override
+    public String getMimeType() {
+        return "application/webpub+json";
+    }
+
+    @Override
+    public NanoHTTPD.Response.IStatus getStatus() {
+        return NanoHTTPD.Response.Status.OK;
+    }
+
+    @Override
+    public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map<String, String> urlParams, NanoHTTPD.IHTTPSession session) {
+        EpubFetcher fetcher = uriResource.initParameter(EpubFetcher.class);
+
+        if (session.getParameters().containsKey("resource")) {
+            String searchQueryPath = session.getParameters().get("resource").get(0);
+            List<Link> spines = fetcher.publication.spines;
+            ObjectMapper objectMapper = new ObjectMapper();
+            try {
+                String json = objectMapper.writeValueAsString(getMediaOverlay(spines, searchQueryPath));
+                return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), json);
+            } catch (JsonProcessingException e) {
+                return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), ResponseStatus.FAILURE_RESPONSE);
+            }
+        } else {
+            return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), ResponseStatus.FAILURE_RESPONSE);
+        }
+    }
+
+    private MediaOverlays getMediaOverlay(List<Link> spines, String searchQueryPath) {
+        for (Link link : spines) {
+            if (link.href.contains(searchQueryPath)) {
+                return link.mediaOverlay;
+            }
+        }
+        return new MediaOverlays();
+    }
+}
diff --git a/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/ResourceHandler.java b/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/ResourceHandler.java
new file mode 100755 (executable)
index 0000000..b697cbe
--- /dev/null
@@ -0,0 +1,185 @@
+package org.readium.r2_streamer.server.handler;
+
+import org.readium.r2_streamer.fetcher.EpubFetcher;
+import org.readium.r2_streamer.fetcher.EpubFetcherException;
+import org.readium.r2_streamer.model.publication.Encryption;
+import org.readium.r2_streamer.model.publication.link.Link;
+import org.readium.r2_streamer.parser.EncryptionDecoder;
+import org.readium.r2_streamer.server.ResponseStatus;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.NanoHTTPD.IHTTPSession;
+import fi.iki.elonen.NanoHTTPD.Method;
+import fi.iki.elonen.NanoHTTPD.Response;
+import fi.iki.elonen.NanoHTTPD.Response.IStatus;
+import fi.iki.elonen.NanoHTTPD.Response.Status;
+import fi.iki.elonen.router.RouterNanoHTTPD.DefaultHandler;
+import fi.iki.elonen.router.RouterNanoHTTPD.UriResource;
+
+import static fi.iki.elonen.NanoHTTPD.MIME_PLAINTEXT;
+
+/**
+ * Created by Shrikant Badwaik on 10-Feb-17.
+ */
+
+public class ResourceHandler extends DefaultHandler {
+    private static final String TAG = "ResourceHandler";
+
+    public ResourceHandler() {
+    }
+
+    @Override
+    public String getMimeType() {
+        return null;
+    }
+
+    private final String[] fonts = {".woff", ".ttf", ".obf", ".woff2", ".eot", ".otf"};
+
+    @Override
+    public String getText() {
+        return ResponseStatus.FAILURE_RESPONSE;
+    }
+
+    @Override
+    public IStatus getStatus() {
+        return Status.OK;
+    }
+
+    @Override
+    public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+        Method method = session.getMethod();
+        String uri = session.getUri();
+
+        System.out.println(TAG + " Method: " + method + ", Url: " + uri);
+
+        try {
+            EpubFetcher fetcher = uriResource.initParameter(EpubFetcher.class);
+            int offset = uri.indexOf("/", 0);
+            int startIndex = uri.indexOf("/", offset + 1);
+            String filePath = uri.substring(startIndex + 1);
+            Link link = fetcher.publication.getResourceMimeType(filePath);
+            String mimeType = link.getTypeLink();
+
+            // If the content is of type html return the response this is done to
+            // skip the check for following font deobfuscation check
+            if (mimeType.equals("application/xhtml+xml")) {
+                return serveResponse(session, fetcher.getDataInputStream(filePath), mimeType);
+            }
+
+            // ********************
+            //  FONT DEOBFUSCATION
+            // ********************
+            if (isFontFile(filePath)) { // Check if the incoming request for the font file is encrypted
+                Encryption encryption = Encryption.getEncryptionFormFontFilePath(
+                        filePath,
+                        fetcher.publication.encryptions);
+                if (encryption != null) { // Decode the font file if encryption exists
+                    return serveResponse(session,
+                            EncryptionDecoder.decode(
+                                    fetcher.publication.metadata.identifier,
+                                    fetcher.getDataInputStream(encryption.getProfile()),
+                                    encryption),
+                            mimeType);
+                }
+            }
+            return serveResponse(session, fetcher.getDataInputStream(filePath), mimeType);
+        } catch (EpubFetcherException e) {
+            System.out.println(TAG + " EpubFetcherException " + e.toString());
+            return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, getMimeType(), ResponseStatus.FAILURE_RESPONSE);
+        }
+    }
+
+    private Response serveResponse(IHTTPSession session, InputStream inputStream, String mimeType) {
+        Response response;
+        String rangeRequest = session.getHeaders().get("range");
+
+        try {
+            // Calculate etag
+            String etag = Integer.toHexString(inputStream.hashCode());
+
+            // Support skipping:
+            long startFrom = 0;
+            long endAt = -1;
+            if (rangeRequest != null) {
+                if (rangeRequest.startsWith("bytes=")) {
+                    rangeRequest = rangeRequest.substring("bytes=".length());
+                    int minus = rangeRequest.indexOf('-');
+                    try {
+                        if (minus > 0) {
+                            startFrom = Long.parseLong(rangeRequest.substring(0, minus));
+                            endAt = Long.parseLong(rangeRequest.substring(minus + 1));
+                        }
+                    } catch (NumberFormatException ignored) {
+                    }
+                }
+            }
+
+            // Change return code and add Content-Range header when skipping is requested
+            long streamLength = inputStream.available();
+            if (rangeRequest != null && startFrom >= 0) {
+                if (startFrom >= streamLength) {
+                    response = createResponse(Response.Status.RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "");
+                    response.addHeader("Content-Range", "bytes 0-0/" + streamLength);
+                    response.addHeader("ETag", etag);
+                } else {
+                    if (endAt < 0) {
+                        endAt = streamLength - 1;
+                    }
+                    long newLen = endAt - startFrom + 1;
+                    if (newLen < 0) {
+                        newLen = 0;
+                    }
+
+                    final long dataLen = newLen;
+                    inputStream.skip(startFrom);
+
+                    response = createResponse(Response.Status.PARTIAL_CONTENT, mimeType, inputStream);
+                    response.addHeader("Content-Length", "" + dataLen);
+                    response.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + streamLength);
+                    response.addHeader("ETag", etag);
+                }
+            } else {
+                if (etag.equals(session.getHeaders().get("if-none-match")))
+                    response = createResponse(Response.Status.NOT_MODIFIED, mimeType, "");
+                else {
+                    response = createResponse(Response.Status.OK, mimeType, inputStream);
+                    response.addHeader("Content-Length", "" + streamLength);
+                    response.addHeader("ETag", etag);
+                }
+            }
+        } catch (IOException | NullPointerException ioe) {
+            response = getResponse("Forbidden: Reading file failed");
+        }
+
+        return (response == null) ? getResponse("Error 404: File not found") : response;
+    }
+
+    private Response createResponse(Status status, String mimeType, InputStream message) {
+        Response response = NanoHTTPD.newChunkedResponse(status, mimeType, message);
+        response.addHeader("Accept-Ranges", "bytes");
+        return response;
+    }
+
+    private Response createResponse(Status status, String mimeType, String message) {
+        Response response = NanoHTTPD.newFixedLengthResponse(status, mimeType, message);
+        response.addHeader("Accept-Ranges", "bytes");
+        return response;
+    }
+
+    private Response getResponse(String message) {
+        return createResponse(Response.Status.OK, "text/plain", message);
+    }
+
+    private boolean isFontFile(String file) {
+        for (String font : fonts) {
+            if (file.endsWith(font)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/SearchQueryHandler.java b/Android/r2-streamer/r2-server/src/main/java/org/readium/r2_streamer/server/handler/SearchQueryHandler.java
new file mode 100755 (executable)
index 0000000..f5ec986
--- /dev/null
@@ -0,0 +1,153 @@
+package org.readium.r2_streamer.server.handler;
+
+//import android.support.annotation.Nullable;
+//import android.util.Log;
+
+import org.readium.r2_streamer.fetcher.EpubFetcher;
+import org.readium.r2_streamer.fetcher.EpubFetcherException;
+import org.readium.r2_streamer.model.publication.link.Link;
+import org.readium.r2_streamer.model.searcher.SearchQueryResults;
+import org.readium.r2_streamer.model.searcher.SearchResult;
+import org.readium.r2_streamer.server.ResponseStatus;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import fi.iki.elonen.NanoHTTPD;
+import fi.iki.elonen.NanoHTTPD.IHTTPSession;
+import fi.iki.elonen.NanoHTTPD.Method;
+import fi.iki.elonen.NanoHTTPD.Response;
+import fi.iki.elonen.NanoHTTPD.Response.IStatus;
+import fi.iki.elonen.NanoHTTPD.Response.Status;
+import fi.iki.elonen.router.RouterNanoHTTPD.DefaultHandler;
+import fi.iki.elonen.router.RouterNanoHTTPD.UriResource;
+
+/**
+ * Created by Shrikant Badwaik on 17-Feb-17.
+ */
+
+public class SearchQueryHandler extends DefaultHandler {
+    private static final String TAG = "SearchQueryHandler";
+    private Response response;
+
+    public SearchQueryHandler() {
+    }
+
+    @Override
+    public String getText() {
+        return ResponseStatus.FAILURE_RESPONSE;
+    }
+
+    @Override
+    public String getMimeType() {
+        return "application/json";
+    }
+
+    @Override
+    public IStatus getStatus() {
+        return Status.NOT_ACCEPTABLE;
+    }
+
+    @Override
+    public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
+        Method method = session.getMethod();
+        String uri = session.getUri();
+
+        //Log.d(TAG, "Method: " + method + ", Url: " + uri);
+
+        try {
+            EpubFetcher fetcher = uriResource.initParameter(EpubFetcher.class);
+
+            String queryParameter = session.getQueryParameterString();
+            int startIndex = queryParameter.indexOf("=");
+            String searchQueryPath = queryParameter.substring(startIndex + 1);
+
+            SearchQueryResults searchQueryResults = new SearchQueryResults();
+            for (Link link : fetcher.publication.spines) {
+                String htmlText = fetcher.getData(link.getHref());
+                if (searchQueryPath.contains("%20")) {
+                    searchQueryPath = searchQueryPath.replaceAll("%20", " ");
+                }
+                Pattern pattern = Pattern.compile(searchQueryPath, Pattern.CASE_INSENSITIVE);
+                Matcher matcher = pattern.matcher(htmlText);
+                while (matcher.find()) {
+                    int start = matcher.start();
+
+                    String prev = getTextBefore(start, htmlText);
+                    //String prev = htmlText.substring(start - 20, start);
+                    String next = getTextAfter(start, searchQueryPath, htmlText);
+                    //String next = htmlText.substring(start + searchQueryPath.length(), (start + searchQueryPath.length()) + 20);
+                    String match = prev.concat(searchQueryPath).concat(next);
+
+                    SearchResult searchResult = new SearchResult();
+                    searchResult.setSearchIndex(start);
+                    searchResult.setResource(link.getHref());
+                    searchResult.setSearchQuery(searchQueryPath);
+                    searchResult.setMatchString(match);
+                    searchResult.setTextBefore(prev);
+                    searchResult.setTextAfter(next);
+
+                    String title = parseChapterTitle(htmlText);
+                    if (title != null) {
+                        searchResult.setTitle(title);
+                    } else {
+                        searchResult.setTitle("Title not available");
+                    }
+                    searchQueryResults.searchResultList.add(searchResult);
+                }
+            }
+            ObjectMapper objectMapper = new ObjectMapper();
+            String json = objectMapper.writeValueAsString(searchQueryResults);
+            response = NanoHTTPD.newFixedLengthResponse(Status.OK, getMimeType(), json);
+            return response;
+        } catch (EpubFetcherException | JsonProcessingException e) {
+            e.printStackTrace();
+            return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, getMimeType(), ResponseStatus.FAILURE_RESPONSE);
+        }
+    }
+
+    //@Nullable
+    private String parseChapterTitle(String searchData) {
+        Document document = Jsoup.parse(searchData);
+        Element h1Element = document.select("h1").first();
+        if (h1Element != null) {
+            return h1Element.text();
+        } else {
+            Element h2Element = document.select("h2").first();
+            if (h2Element != null) {
+                return h2Element.text();
+            } else {
+                Element titleElement = document.select("title").first();
+                if (titleElement != null) {
+                    return titleElement.text();
+                }
+            }
+        }
+        return null;
+    }
+
+    private String getTextBefore(int start, String htmlString) {
+        int beginIndex = start - 20;
+        if (beginIndex >= 0 && beginIndex < htmlString.length()) {
+            return htmlString.substring(beginIndex, start);
+        } else {
+            return htmlString.substring(0, start);
+        }
+    }
+
+    private String getTextAfter(int start, String searchQueryPath, String htmlString) {
+        int beginIndex = start + searchQueryPath.length();
+        if ((beginIndex + 20) > htmlString.length()) {
+            return htmlString.substring(beginIndex);
+        } else {
+            return htmlString.substring(beginIndex, beginIndex + 20);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/sample/.gitignore b/Android/r2-streamer/sample/.gitignore
new file mode 100755 (executable)
index 0000000..796b96d
--- /dev/null
@@ -0,0 +1 @@
+/build
diff --git a/Android/r2-streamer/sample/build.gradle b/Android/r2-streamer/sample/build.gradle
new file mode 100755 (executable)
index 0000000..4456fd8
--- /dev/null
@@ -0,0 +1,44 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 25
+    buildToolsVersion "25.0.0"
+
+    defaultConfig {
+        applicationId "com.codetoart.sample"
+        minSdkVersion 17
+        targetSdkVersion 25
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+    packagingOptions {
+        exclude 'libs/jackson-core-asl-1.5.0.jar'
+        exclude 'libs/jackson-mapper-asl-1.5.0.jar'
+        exclude 'META-INF/ASL2.0'
+        exclude 'META-INF/LICENSE'
+        exclude 'META-INF/NOTICE'
+    }
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+
+    compile project(':r2-server')
+    compile project(':r2-parser')
+    compile project(':r2-fetcher')
+
+    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+        exclude group: 'com.android.support', module: 'support-annotations'
+    })
+    compile 'com.android.support:appcompat-v7:25.2.0'
+    testCompile 'junit:junit:4.12'
+}
diff --git a/Android/r2-streamer/sample/proguard-rules.pro b/Android/r2-streamer/sample/proguard-rules.pro
new file mode 100755 (executable)
index 0000000..e181567
--- /dev/null
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:\android-sdk-windows/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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 *;
+#}
diff --git a/Android/r2-streamer/sample/src/androidTest/java/org/readium/sample/ExampleInstrumentedTest.java b/Android/r2-streamer/sample/src/androidTest/java/org/readium/sample/ExampleInstrumentedTest.java
new file mode 100755 (executable)
index 0000000..a8dc0d3
--- /dev/null
@@ -0,0 +1,26 @@
+package org.readium.sample;
+
+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.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() throws Exception {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getTargetContext();
+
+        assertEquals("com.codetoart.sample", appContext.getPackageName());
+    }
+}
diff --git a/Android/r2-streamer/sample/src/main/AndroidManifest.xml b/Android/r2-streamer/sample/src/main/AndroidManifest.xml
new file mode 100755 (executable)
index 0000000..1de91f5
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.readium.sample">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity android:name="org.readium.sample.TestActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/Android/r2-streamer/sample/src/main/assets/AlmaTademaPortfolio.cbz b/Android/r2-streamer/sample/src/main/assets/AlmaTademaPortfolio.cbz
new file mode 100755 (executable)
index 0000000..ae5cafc
Binary files /dev/null and b/Android/r2-streamer/sample/src/main/assets/AlmaTademaPortfolio.cbz differ
diff --git a/Android/r2-streamer/sample/src/main/assets/SmokeTestFXL.epub b/Android/r2-streamer/sample/src/main/assets/SmokeTestFXL.epub
new file mode 100755 (executable)
index 0000000..d41af0a
Binary files /dev/null and b/Android/r2-streamer/sample/src/main/assets/SmokeTestFXL.epub differ
diff --git a/Android/r2-streamer/sample/src/main/java/org/readium/sample/Constant.java b/Android/r2-streamer/sample/src/main/java/org/readium/sample/Constant.java
new file mode 100755 (executable)
index 0000000..9dd15f5
--- /dev/null
@@ -0,0 +1,15 @@
+package org.readium.sample;
+
+/**
+ * @author gautam chibde on 8/6/17.
+ */
+
+public interface Constant {
+    int PORT_NUMBER = 3000;
+
+    String EPUB_TITLE = "SmokeTestFXL.epub";
+
+    String BASE_URL = "http://127.0.0.1";
+    String URL = BASE_URL + ":" + PORT_NUMBER + "/" + EPUB_TITLE;
+    String MANIFEST = "/manifest";
+}
diff --git a/Android/r2-streamer/sample/src/main/java/org/readium/sample/TestActivity.java b/Android/r2-streamer/sample/src/main/java/org/readium/sample/TestActivity.java
new file mode 100755 (executable)
index 0000000..77af68d
--- /dev/null
@@ -0,0 +1,267 @@
+package org.readium.sample;
+
+import android.Manifest;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.readium.sample.R;
+
+import org.readium.r2_streamer.model.container.Container;
+import org.readium.r2_streamer.model.container.EpubContainer;
+import org.readium.r2_streamer.model.publication.EpubPublication;
+import org.readium.r2_streamer.model.publication.link.Link;
+import org.readium.r2_streamer.model.searcher.SearchQueryResults;
+import org.readium.r2_streamer.model.searcher.SearchResult;
+import org.readium.r2_streamer.server.EpubServer;
+import org.readium.r2_streamer.server.EpubServerSingleton;
+import org.readium.sample.adapters.SearchListAdapter;
+import org.readium.sample.adapters.SpineListAdapter;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
+    private static final String ROOT_EPUB_PATH = Environment.getExternalStorageDirectory().getPath() + "/R2StreamerSample/";
+    private static final int WRITE_EXST = 100;
+    private final String TAG = "TestActivity";
+    private EpubServer mEpubServer;
+
+    private EditText searchBar;
+    private ListView listView;
+    private List<Link> manifestItemList = new ArrayList<>();
+    private List<SearchResult> searchList = new ArrayList<>();
+    private ProgressDialog progressDialog;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        progressDialog = new ProgressDialog(this);
+        progressDialog.setMessage("Loading.... ");
+        progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+        progressDialog.setIndeterminate(true);
+        progressDialog.setCancelable(false);
+        progressDialog.setCanceledOnTouchOutside(false);
+
+        askForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, WRITE_EXST);
+        setContentView(R.layout.activity_sample_main);
+        searchBar = (EditText) findViewById(R.id.searchField);
+        listView = (ListView) findViewById(R.id.list);
+
+        copyEpubFromAssetsToSdCard(Constant.EPUB_TITLE);
+        startServer();
+
+        Log.d(TAG, "Server is running. Point your browser at " + Constant.URL);
+    }
+
+    private void startServer() {
+        try {
+            mEpubServer = EpubServerSingleton.getEpubServerInstance(Constant.PORT_NUMBER);
+            mEpubServer.start();
+        } catch (IOException e) {
+            Log.e(TAG, "startServer IOException " + e.toString());
+        }
+    }
+
+    public void find(View view) throws IOException {
+        addEpub();
+        searchList.clear();
+        String searchQuery = searchBar.getText().toString();
+        if (!searchQuery.isEmpty()) {
+            progressDialog.show();
+            if (searchQuery.contains(" ")) {
+                searchQuery = searchQuery.replaceAll(" ", "%20");
+            }
+            if (searchQuery.length() != 0) {
+                String urlString = Constant.URL + "/search?query=" + searchQuery;
+                new SearchListTask().execute(urlString);
+            }
+        } else {
+            searchBar.requestFocus();
+            searchBar.setError("Enter search query");
+        }
+    }
+
+    private void addEpub() throws IOException {
+        String path = ROOT_EPUB_PATH + Constant.EPUB_TITLE;
+        //DirectoryContainer directoryContainer = new DirectoryContainer(path);
+        Container epubContainer = new EpubContainer(path);
+        mEpubServer.addEpub(epubContainer, "/" + Constant.EPUB_TITLE);
+    }
+
+    public void show(View view) throws IOException {
+        progressDialog.show();
+        addEpub();
+        manifestItemList.clear();
+        String urlString = Constant.URL + Constant.MANIFEST;
+        Log.i(TAG, "urlString: " + urlString);
+        new SpineListTask().execute(urlString);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mEpubServer != null && mEpubServer.isAlive()) {
+            mEpubServer.stop();
+            EpubServerSingleton.resetServerInstance();
+        }
+
+        Log.d(TAG, "Server has been stopped");
+    }
+
+    public void copyEpubFromAssetsToSdCard(String epubFileName) {
+        try {
+            File dir = new File(ROOT_EPUB_PATH);
+            if (!dir.exists()) dir.mkdirs();
+            File file = new File(dir, epubFileName);
+            file.createNewFile();
+
+            FileOutputStream fos = new FileOutputStream(file);
+            InputStream fis = getAssets().open(Constant.EPUB_TITLE);
+            byte[] b = new byte[1024];
+            int i;
+            while ((i = fis.read(b)) != -1) {
+                fos.write(b, 0, i);
+            }
+            fos.flush();
+            fos.close();
+            fis.close();
+        } catch (IOException e) {
+            Log.e(TAG, "copyEpubFromAssetsToSdCard IOException " + e.toString());
+        }
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
+        String urlString = Constant.URL + "/" + manifestItemList.get(position).getHref();
+        //String urlString = "http://127.0.0.1:8080/BARRETT_GUIDE.epub/" + searchList.get(position).getResource();
+        Uri uri = Uri.parse(urlString);
+        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+        startActivity(intent);
+    }
+
+    class SpineListTask extends AsyncTask<String, Void, EpubPublication> {
+        @Override
+        protected EpubPublication doInBackground(String... urls) {
+            String strUrl = urls[0];
+
+            try {
+                URL url = new URL(strUrl);
+                URLConnection urlConnection = url.openConnection();
+                InputStream inputStream = urlConnection.getInputStream();
+                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+                StringBuilder stringBuilder = new StringBuilder();
+                String line;
+                while ((line = bufferedReader.readLine()) != null) {
+                    stringBuilder.append(line);
+                }
+
+                Log.d("TestActivity", "EpubPublication => " + stringBuilder.toString());
+
+                ObjectMapper objectMapper = new ObjectMapper();
+                objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+                return objectMapper.readValue(stringBuilder.toString(), EpubPublication.class);
+            } catch (IOException e) {
+                Log.e(TAG, "SpineListTask error " + e);
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(EpubPublication epubPublication) {
+            manifestItemList = epubPublication.spines;
+            SpineListAdapter arrayAdapter = new SpineListAdapter(TestActivity.this, manifestItemList);
+            listView.setAdapter(arrayAdapter);
+            listView.setOnItemClickListener(TestActivity.this);
+            cancel(true);
+            progressDialog.dismiss();
+        }
+    }
+
+    class SearchListTask extends AsyncTask<String, Void, SearchQueryResults> {
+
+        @Override
+        protected SearchQueryResults doInBackground(String... urls) {
+            String strUrl = urls[0];
+            try {
+                URL url = new URL(strUrl);
+                HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+                urlConnection.setRequestMethod("GET");
+                InputStream inputStream = urlConnection.getInputStream();
+                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+                StringBuilder stringBuilder = new StringBuilder();
+                String line;
+                while ((line = bufferedReader.readLine()) != null) {
+                    stringBuilder.append(line);
+                }
+
+                ObjectMapper objectMapper = new ObjectMapper();
+                objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+                return objectMapper.readValue(stringBuilder.toString(), SearchQueryResults.class);
+            } catch (IOException e) {
+                Log.e(TAG, "SearchListTask IOException " + e.toString());
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(SearchQueryResults results) {
+            searchList = results.searchResultList;
+            SearchListAdapter adapter = new SearchListAdapter(TestActivity.this, searchList);
+            listView.setAdapter(adapter);
+            listView.setOnItemClickListener(TestActivity.this);
+            cancel(true);
+            progressDialog.dismiss();
+        }
+    }
+
+    private void askForPermission(String permission, Integer requestCode) {
+        if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
+            if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
+                ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
+            } else {
+                ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
+            }
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        if (ActivityCompat.checkSelfPermission(this, permissions[0]) == PackageManager.PERMISSION_GRANTED) {
+            if (requestCode == WRITE_EXST) {
+                Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show();
+            }
+        } else {
+            Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
+            finish();
+        }
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/sample/src/main/java/org/readium/sample/adapters/SearchListAdapter.java b/Android/r2-streamer/sample/src/main/java/org/readium/sample/adapters/SearchListAdapter.java
new file mode 100755 (executable)
index 0000000..c56b403
--- /dev/null
@@ -0,0 +1,65 @@
+package org.readium.sample.adapters;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.readium.sample.R;
+
+import org.readium.r2_streamer.model.searcher.SearchResult;
+
+import java.util.List;
+
+/**
+ * Created by Shrikant Badwaik on 17-Feb-17.
+ */
+
+public class SearchListAdapter extends ArrayAdapter<String> {
+    private Context context;
+    private List<SearchResult> list;
+    private TextView view_1, view_2;
+
+    public SearchListAdapter(Context context, List<SearchResult> list) {
+        super(context, 0);
+        this.context = context;
+        this.list = list;
+    }
+
+    @Override
+    public int getCount() {
+        return list.size();
+    }
+
+    @NonNull
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        LinearLayout layout = null;
+        if (convertView == null) {
+            LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            layout = (LinearLayout) layoutInflater.inflate(R.layout.searchlist_adapter_resource, null);
+        } else {
+            layout = (LinearLayout) convertView;
+        }
+
+        view_1 = (TextView) layout.findViewById(R.id.titleText);
+        view_2 = (TextView) layout.findViewById(R.id.matchingText);
+
+        SearchResult searchResult = list.get(position);
+        view_1.setText(searchResult.getTitle());
+        view_2.setText(stripHtmlTags(searchResult.getMatchString()));
+
+        return layout;
+    }
+
+    private String stripHtmlTags(String htmlText) {
+        String plainText = htmlText.replaceAll("<[^>]+>", "");
+        plainText = plainText.replaceAll("<[^>]*", "");
+        plainText = plainText.replaceAll("[^<]*>", "");
+        return plainText;
+    }
+}
diff --git a/Android/r2-streamer/sample/src/main/java/org/readium/sample/adapters/SpineListAdapter.java b/Android/r2-streamer/sample/src/main/java/org/readium/sample/adapters/SpineListAdapter.java
new file mode 100755 (executable)
index 0000000..8fb98e0
--- /dev/null
@@ -0,0 +1,56 @@
+package org.readium.sample.adapters;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.readium.sample.R;
+
+import org.readium.r2_streamer.model.publication.link.Link;
+
+import java.util.List;
+
+/**
+ * Created by Shrikant Badwaik on 24-Feb-17.
+ */
+
+public class SpineListAdapter extends ArrayAdapter<String> {
+    private Context context;
+    private List<Link> list;
+    private TextView view_1;
+
+    public SpineListAdapter(Context context, List<Link> list) {
+        super(context, 0);
+        this.context = context;
+        this.list = list;
+    }
+
+    @Override
+    public int getCount() {
+        return list.size();
+    }
+
+    @NonNull
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        LinearLayout layout = null;
+        if (convertView == null) {
+            LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            layout = (LinearLayout) layoutInflater.inflate(R.layout.spinelist_adapter_resource, null);
+        } else {
+            layout = (LinearLayout) convertView;
+        }
+
+        view_1 = (TextView) layout.findViewById(R.id.spineTextView);
+
+        Link link = list.get(position);
+        view_1.setText(link.getHref());
+
+        return layout;
+    }
+}
diff --git a/Android/r2-streamer/sample/src/main/res/layout/activity_sample_main.xml b/Android/r2-streamer/sample/src/main/res/layout/activity_sample_main.xml
new file mode 100755 (executable)
index 0000000..a7b193b
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/activity_main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    tools:context="com.readium.sample.org.readium.sample.TestActivity">
+
+    <EditText
+        android:id="@+id/searchField"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <Button
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:onClick="find"
+        android:text="find" />
+
+    <Button
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:onClick="show"
+        android:text="show" />
+
+    <ListView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+</LinearLayout>
diff --git a/Android/r2-streamer/sample/src/main/res/layout/searchlist_adapter_resource.xml b/Android/r2-streamer/sample/src/main/res/layout/searchlist_adapter_resource.xml
new file mode 100755 (executable)
index 0000000..d75c1ab
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/titleText"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="20dp"/>
+
+    <TextView
+        android:id="@+id/matchingText"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="15dp"
+        android:layout_marginTop="3dp"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/Android/r2-streamer/sample/src/main/res/layout/spinelist_adapter_resource.xml b/Android/r2-streamer/sample/src/main/res/layout/spinelist_adapter_resource.xml
new file mode 100755 (executable)
index 0000000..d1cd568
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/spineTextView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="10dp"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/Android/r2-streamer/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/Android/r2-streamer/sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..cde69bc
Binary files /dev/null and b/Android/r2-streamer/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Android/r2-streamer/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/Android/r2-streamer/sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..c133a0c
Binary files /dev/null and b/Android/r2-streamer/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Android/r2-streamer/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/Android/r2-streamer/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..bfa42f0
Binary files /dev/null and b/Android/r2-streamer/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Android/r2-streamer/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Android/r2-streamer/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..324e72c
Binary files /dev/null and b/Android/r2-streamer/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Android/r2-streamer/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Android/r2-streamer/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755 (executable)
index 0000000..aee44e1
Binary files /dev/null and b/Android/r2-streamer/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/Android/r2-streamer/sample/src/main/res/values-w820dp/dimens.xml b/Android/r2-streamer/sample/src/main/res/values-w820dp/dimens.xml
new file mode 100755 (executable)
index 0000000..63fc816
--- /dev/null
@@ -0,0 +1,6 @@
+<resources>
+    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+         (such as screen margins) for screens with more than 820dp of available width. This
+         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+    <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/Android/r2-streamer/sample/src/main/res/values/colors.xml b/Android/r2-streamer/sample/src/main/res/values/colors.xml
new file mode 100755 (executable)
index 0000000..3ab3e9c
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#3F51B5</color>
+    <color name="colorPrimaryDark">#303F9F</color>
+    <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/Android/r2-streamer/sample/src/main/res/values/dimens.xml b/Android/r2-streamer/sample/src/main/res/values/dimens.xml
new file mode 100755 (executable)
index 0000000..47c8224
--- /dev/null
@@ -0,0 +1,5 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/Android/r2-streamer/sample/src/main/res/values/strings.xml b/Android/r2-streamer/sample/src/main/res/values/strings.xml
new file mode 100755 (executable)
index 0000000..f3fcd5f
--- /dev/null
@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">R2-Streamer Sample</string>
+</resources>
diff --git a/Android/r2-streamer/sample/src/main/res/values/styles.xml b/Android/r2-streamer/sample/src/main/res/values/styles.xml
new file mode 100755 (executable)
index 0000000..5885930
--- /dev/null
@@ -0,0 +1,11 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>
diff --git a/Android/r2-streamer/sample/src/test/java/org/readium/sample/ExampleUnitTest.java b/Android/r2-streamer/sample/src/test/java/org/readium/sample/ExampleUnitTest.java
new file mode 100755 (executable)
index 0000000..23fcd24
--- /dev/null
@@ -0,0 +1,17 @@
+package org.readium.sample;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() throws Exception {
+        assertEquals(4, 2 + 2);
+    }
+}
\ No newline at end of file
diff --git a/Android/r2-streamer/settings.gradle b/Android/r2-streamer/settings.gradle
new file mode 100755 (executable)
index 0000000..0e3502a
--- /dev/null
@@ -0,0 +1 @@
+include ':sample', ':r2-parser', ':r2-server', ':r2-fetcher'
diff --git a/Android/settings.gradle b/Android/settings.gradle
new file mode 100644 (file)
index 0000000..7904c21
--- /dev/null
@@ -0,0 +1,2 @@
+include ':app', ':folioreader', ':webViewMarker', ':r2-streamer', ':r2-streamer:r2-fetcher', ':r2-streamer:r2-parser',
+        ':r2-streamer:r2-server'
diff --git a/Android/webViewMarker/build.gradle b/Android/webViewMarker/build.gradle
new file mode 100755 (executable)
index 0000000..32ac48d
--- /dev/null
@@ -0,0 +1,47 @@
+apply plugin: 'com.android.library'
+
+ext {
+    bintrayRepo = 'maven'
+    bintrayName = 'folioreader'
+
+    publishedGroupId = 'com.folioreader'
+    libraryName = 'WebViewMarker'
+    artifact = 'webViewMarker'
+
+    libraryDescription = 'An epub reader for Android'
+
+    siteUrl = 'https://github.com/FolioReader/FolioReader-Android'
+    gitUrl = 'https://github.com/FolioReader/FolioReader-Android.git'
+
+    libraryVersion = '0.3.1'
+
+    developerId = 'mobisystech'
+    developerName = 'Folio Reader'
+    developerEmail = 'mahavir@codetoart.com'
+
+    licenseName = 'FreeBSD License'
+    licenseUrl = 'https://en.wikipedia.org/wiki/FreeBSD_Documentation_License#License'
+    allLicenses = ["FreeBSD"]
+}
+
+android {
+    compileSdkVersion 27
+    buildToolsVersion '27.0.3'
+
+    defaultConfig {
+        minSdkVersion 14
+        targetSdkVersion 27
+        versionCode 1
+        versionName "1.0"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+        }
+    }
+}
+
+apply from: '../folioreader/bintray/installv1.gradle'
+apply from: '../folioreader/bintray/bintrayv1.gradle'
diff --git a/Android/webViewMarker/src/main/AndroidManifest.xml b/Android/webViewMarker/src/main/AndroidManifest.xml
new file mode 100755 (executable)
index 0000000..5582775
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.bossturban.webviewmarker" android:versionCode="4" android:versionName="0.1.3">
+  <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="19"/>
+</manifest>
diff --git a/Android/webViewMarker/src/main/assets/android.selection.js b/Android/webViewMarker/src/main/assets/android.selection.js
new file mode 100755 (executable)
index 0000000..42a5597
--- /dev/null
@@ -0,0 +1,255 @@
+// Namespace
+var android = {};
+android.selection = {};
+
+android.selection.selectionStartRange = null;
+android.selection.selectionEndRange = null;
+
+/** The last point touched by the user. { 'x': xPoint, 'y': yPoint } */
+android.selection.lastTouchPoint = null;
+
+
+/**
+ * Starts the touch and saves the given x and y coordinates as last touch point
+ */
+android.selection.startTouch = function(x, y) {
+    android.selection.lastTouchPoint = {'x': x, 'y': y};
+};
+
+/**
+ *  Checks to see if there is a selection.
+ *
+ *  @return boolean
+ */
+android.selection.hasSelection = function() {
+    return window.getSelection().toString().length > 0;
+};
+
+/**
+ *  Clears the current selection.
+ */
+android.selection.clearSelection = function() {
+    try {
+        // if current selection clear it.
+        var sel = window.getSelection();
+        sel.removeAllRanges();
+    }
+    catch (e) {
+        window.TextSelection.jsError(e);
+    }
+};
+
+/**
+ *  Handles the long touch action by selecting the last touched element.
+ */
+android.selection.longTouch = function() {
+    try {
+        android.selection.clearSelection();
+        var sel = window.getSelection();
+        var range = document.caretRangeFromPoint(android.selection.lastTouchPoint.x, android.selection.lastTouchPoint.y);
+        range.expand("word");
+        var text = range.toString();
+        if (text.length == 1) {
+            var baseKind = jpntext.kind(text);
+            if (baseKind != jpntext.KIND['ascii']) {
+                try {
+                    do {
+                        range.setEnd(range.endContainer, range.endOffset + 1);
+                        text = range.toString();
+                        var kind = jpntext.kind(text);
+                    } while (baseKind == kind);
+                    range.setEnd(range.endContainer, range.endOffset - 1);
+                }
+                catch (e) {
+                }
+                try {
+                    do {
+                        range.setStart(range.startContainer, range.startOffset - 1);
+                        text = range.toString();
+                        var kind = jpntext.kind(text);
+                    } while (baseKind == kind);
+                    range.setStart(range.startContainer, range.startOffset + 1);
+                }
+                catch (e) {
+                }
+            }
+        }
+        if (text.length > 0) {
+            sel.addRange(range);
+            android.selection.saveSelectionStart();
+            android.selection.saveSelectionEnd();
+            android.selection.selectionChanged(true);
+        }
+     }
+     catch (err) {
+        window.TextSelection.jsError(err);
+     }
+};
+
+/**
+ * Tells the app to show the context menu.
+ */
+android.selection.selectionChanged = function(isReallyChanged) {
+    try {
+        var sel = window.getSelection();
+        if (!sel) {
+            return;
+        }
+        var range = sel.getRangeAt(0);
+
+        // Add spans to the selection to get page offsets
+        var selectionStart = $("<span id=\"selectionStart\">&#xfeff;</span>");
+        var selectionEnd = $("<span id=\"selectionEnd\"></span>");
+
+        var startRange = document.createRange();
+        startRange.setStart(range.startContainer, range.startOffset);
+        startRange.insertNode(selectionStart[0]);
+
+        var endRange = document.createRange();
+        endRange.setStart(range.endContainer, range.endOffset);
+        endRange.insertNode(selectionEnd[0]);
+
+        var handleBounds = "{'left': " + (selectionStart.offset().left) + ", ";
+        handleBounds += "'top': " + (selectionStart.offset().top + selectionStart.height()) + ", ";
+        handleBounds += "'right': " + (selectionEnd.offset().left) + ", ";
+        handleBounds += "'bottom': " + (selectionEnd.offset().top + selectionEnd.height()) + "}";
+
+        // Pull the spans
+        selectionStart.remove();
+        selectionEnd.remove();
+
+        // Reset range
+        sel.removeAllRanges();
+        sel.addRange(range);
+
+        // Rangy
+        var rangyRange = android.selection.getRange();
+
+        // Text to send to the selection
+        var text = window.getSelection().toString();
+
+        // Set the content width
+        window.TextSelection.setContentWidth(document.body.clientWidth);
+
+        // Tell the interface that the selection changed
+        window.TextSelection.selectionChanged(rangyRange, text, handleBounds, isReallyChanged);
+    }
+    catch (e) {
+        window.TextSelection.jsError(e);
+    }
+};
+
+android.selection.getRange = function() {
+    var serializedRangeSelected = rangy.serializeSelection();
+    var serializerModule = rangy.modules.Serializer;
+    if (serializedRangeSelected != '') {
+        if (rangy.supported && serializerModule && serializerModule.supported) {
+            var beginingCurly = serializedRangeSelected.indexOf("{");
+            serializedRangeSelected = serializedRangeSelected.substring(0, beginingCurly);
+            return serializedRangeSelected;
+        }
+    }
+}
+
+/**
+ * Returns the last touch point as a readable string.
+ */
+android.selection.lastTouchPointString = function(){
+    if (android.selection.lastTouchPoint == null)
+        return "undefined";
+    return "{" + android.selection.lastTouchPoint.x + "," + android.selection.lastTouchPoint.y + "}";
+};
+
+android.selection.saveSelectionStart = function(){
+    try {
+        // Save the starting point of the selection
+        var sel = window.getSelection();
+        var range = sel.getRangeAt(0);
+        var saveRange = document.createRange();
+        saveRange.setStart(range.startContainer, range.startOffset);
+        android.selection.selectionStartRange = saveRange;
+    }
+    catch (e) {
+        window.TextSelection.jsError(e);
+    }
+};
+
+android.selection.saveSelectionEnd = function(){
+    try {
+        // Save the end point of the selection
+        var sel = window.getSelection();
+        var range = sel.getRangeAt(0);
+        var saveRange = document.createRange();
+        saveRange.setStart(range.endContainer, range.endOffset);
+        android.selection.selectionEndRange = saveRange;
+    }
+    catch (e) {
+        window.TextSelection.jsError(e);
+    }
+};
+
+/**
+ * Sets the last caret position for the start handle.
+ */
+android.selection.setStartPos = function(x, y){
+    try {
+        android.selection.selectBetweenHandles(document.caretRangeFromPoint(x, y), android.selection.selectionEndRange);
+    }
+    catch (e) {
+        window.TextSelection.jsError(e);
+    }
+};
+
+/**
+ * Sets the last caret position for the end handle.
+ */
+android.selection.setEndPos = function(x, y){
+    try {
+        android.selection.selectBetweenHandles(android.selection.selectionStartRange, document.caretRangeFromPoint(x, y));
+    }
+    catch (e) {
+        window.TextSelection.jsError(e);
+    }
+};
+
+android.selection.restoreStartEndPos = function() {
+    try {
+        android.selection.selectBetweenHandles(android.selection.selectionEndRange, android.selection.selectionStartRange);
+    }
+    catch (e) {
+        window.TextSelection.jsError(e);
+    }
+};
+
+/**
+ *  Selects all content between the two handles
+ */
+android.selection.selectBetweenHandles = function(startCaret, endCaret) {
+    try {
+        if (startCaret && endCaret) {
+            var rightOrder = startCaret.compareBoundaryPoints(Range.START_TO_END, endCaret) <= 0;
+            if (rightOrder) {
+                android.selection.selectionStartRange = startCaret;
+                android.selection.selectionEndRange = endCaret;
+            }
+            else {
+                startCaret = android.selection.selectionStartRange;
+                endCaret = android.selection.selectionEndRange;
+            }
+            var range = document.createRange();
+            range.setStart(startCaret.startContainer, startCaret.startOffset);
+            range.setEnd(endCaret.startContainer, endCaret.startOffset);
+            android.selection.clearSelection();
+            var selection = window.getSelection();
+            selection.addRange(range);
+            android.selection.selectionChanged(rightOrder);
+        }
+        else {
+            android.selection.selectionStartRange = startCaret;
+            android.selection.selectionEndRange = endCaret;
+        }
+    }
+    catch (e) {
+        window.TextSelection.jsError(e);
+    }
+};
diff --git a/Android/webViewMarker/src/main/assets/content.html b/Android/webViewMarker/src/main/assets/content.html
new file mode 100755 (executable)
index 0000000..0c954e0
--- /dev/null
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8"/>
+<title>content</title>
+<link rel="stylesheet" href="css/sample.css"/>
+<script src='jquery-1.8.3.js'></script>
+<script src='jpntext.js'></script>
+<script src='rangy-core.js'></script>
+<script src='rangy-serializer.js'></script>
+<script src='android.selection.js'></script>
+</head>
+<body>
+<p>
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras pellentesque, dolor nec luctus ullamcorper, massa quam interdum metus, ac ultricies mauris risus nec purus. Fusce et nunc mi, ut consequat velit. Cras orci sapien, tincidunt sed iaculis ac, commodo sed neque. Sed gravida, quam id imperdiet venenatis, odio erat faucibus nisi, sed imperdiet velit nulla nec risus. Duis eget vehicula nibh. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec tincidunt, augue vitae feugiat cursus, nunc nunc volutpat ligula, ac molestie purus lectus sit amet mauris. Nunc nec felis tortor, a dignissim eros. Vivamus diam mauris, scelerisque molestie bibendum ut, dignissim quis neque. Aliquam ac turpis mi. Sed pretium interdum orci, sed semper lorem viverra id. Vivamus diam eros, convallis sit amet facilisis in, facilisis eget nibh. Aenean porttitor, neque nec ultrices laoreet, dolor metus cursus purus, non auctor lectus urna a enim. In nec lectus nunc, ac commodo arcu. Proin bibendum ligula non mi bibendum porta. Nam ac metus a magna tristique euismod eget vitae purus.
+</p>
+<p>
+Vestibulum tincidunt, lectus at bibendum commodo, lectus velit commodo libero, et fermentum odio arcu sit amet dui. Quisque feugiat augue in erat scelerisque non mollis arcu facilisis. Nunc a dui sapien. Quisque quis nisi eu velit tincidunt placerat. Nunc vitae eros erat. Duis posuere diam ut orci adipiscing a ullamcorper est sollicitudin. Cras ipsum dolor, commodo viverra aliquet vel, pulvinar at sapien. Suspendisse eget justo et neque bibendum tempor. Morbi aliquet enim id arcu convallis cursus. Curabitur pellentesque condimentum dolor non sagittis. Ut tempor, erat in ullamcorper porta, purus erat tincidunt felis, ac auctor mauris libero nec purus. Praesent nisl justo, rutrum at rhoncus at, accumsan at mi. Fusce hendrerit imperdiet nulla a accumsan. Maecenas ut mi ac libero bibendum ullamcorper.
+</p>
+<p>
+Nam ac est nunc. Suspendisse faucibus dictum lacus, sed tincidunt erat laoreet id. Cras leo dui, sodales vitae blandit quis, placerat in arcu. Nunc semper odio id dolor bibendum vel euismod sapien malesuada. Vestibulum metus purus, consequat sed varius ornare, feugiat ac lectus. Suspendisse consectetur ipsum a enim aliquet a pellentesque ipsum condimentum. Etiam molestie, augue id consectetur bibendum, tellus ante vestibulum tellus, vitae aliquam turpis ipsum et elit. Aenean ipsum ante, eleifend in mollis at, molestie ac augue. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec dui justo, consequat semper mollis sit amet, scelerisque vitae enim. Nullam nec odio et tortor sodales posuere. Mauris urna lorem, suscipit eu venenatis eu, consectetur ut purus. Vestibulum pharetra interdum convallis. Donec ac leo felis.
+</p>
+<p>
+Mauris vulputate magna eget ligula fermentum eu molestie justo vulputate. Pellentesque condimentum, sem a sollicitudin luctus, lorem nunc dictum risus, a tincidunt tortor metus nec risus. Nunc ultricies consectetur accumsan. Etiam placerat aliquam tortor id lacinia. Vestibulum quis sem non urna venenatis condimentum. Vestibulum tempor sapien quam, et vulputate elit. Sed convallis mauris sed turpis elementum vitae pretium est tempus. Praesent congue viverra pharetra. Etiam accumsan congue sapien eu molestie.
+</p>
+<p>
+Maecenas laoreet egestas tellus vestibulum gravida. Nulla rutrum dui at purus viverra et pretium diam fringilla. Sed nibh tortor, interdum ac egestas in, aliquam eu erat. Nunc convallis est posuere ipsum blandit luctus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur quam eros, hendrerit quis iaculis ut, rhoncus vel elit. Aenean sed ante et urna semper eleifend. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi mollis lacus eu nulla vestibulum consectetur lobortis risus pretium.
+</p>
+<p>
+Mauris scelerisque imperdiet venenatis. Suspendisse bibendum ullamcorper massa, vitae aliquam ligula sagittis at. Nullam rutrum justo nec tellus egestas eleifend. Suspendisse euismod quam in neque laoreet aliquam. Nunc non massa quis magna gravida ornare sed vitae tortor. Donec fringilla euismod accumsan. Mauris condimentum libero eget nisi sodales fringilla.
+</p>
+<p>
+Suspendisse eget tellus nibh, eu accumsan nibh. Aenean a metus ut leo dapibus lacinia. Vestibulum vitae ante nec urna aliquet posuere sollicitudin vitae mauris. Pellentesque dapibus dapibus nisi, fringilla placerat metus aliquet id. Mauris imperdiet ornare tellus sed ornare. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus elementum fringilla molestie. Cras ac enim at libero condimentum eleifend. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;
+</p>
+<p>
+Maecenas vel hendrerit massa. Maecenas quis augue turpis. Fusce scelerisque gravida nisl vitae auctor. Donec porta, diam id fringilla convallis, risus elit ultrices enim, nec auctor mi justo ut urna. Integer consectetur, sem sed molestie adipiscing, ligula lectus convallis nisl, eget pretium ante nulla non sem. Nulla magna metus, ornare at facilisis sit amet, ultrices id erat. Mauris gravida risus ac augue vulputate imperdiet porta tortor fringilla. Pellentesque cursus aliquam velit at mattis. Aliquam molestie fringilla urna, eget convallis sem hendrerit in. Cras pretium, nunc et aliquam ullamcorper, justo augue cursus turpis, vitae volutpat urna mi sit amet nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</p>
+<p>
+Donec iaculis aliquam nulla sodales sodales. Nunc quis egestas ipsum. Suspendisse accumsan tortor eu lacus sodales eu venenatis lectus varius. Vestibulum nisl mi, vulputate quis pulvinar eu, malesuada vitae dui. Ut eget nulla ipsum. Donec in risus laoreet arcu venenatis aliquam. Curabitur orci augue, pulvinar quis sollicitudin et, laoreet a turpis. Donec a nunc id elit volutpat euismod sed at lorem.
+</p>
+<p>
+Phasellus arcu augue, rhoncus nec luctus quis, pharetra sit amet ante. Phasellus sed dui sit amet lacus auctor varius ac a arcu. Nullam eu congue ligula. Duis pretium nisi et tellus faucibus commodo. Vivamus dapibus imperdiet condimentum. Fusce at velit arcu, ac imperdiet mauris. Donec sit amet metus libero. Integer ac enim elit. Nulla quis erat urna. Morbi lobortis ligula vel sem placerat porttitor. Vestibulum in velit nulla, in malesuada sapien. Pellentesque sed sapien urna. Maecenas sodales felis sed nisl dictum euismod. Cras vitae sagittis elit.
+</p>
+<p>
+あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、うつくしい森で飾られたモリーオ市、郊外のぎらぎらひかる草の波。
+</p>
+<p>
+またそのなかでいっしょになったたくさんのひとたち、ファゼーロとロザーロ、羊飼のミーロや、顔の赤いこどもたち、地主のテーモ、山猫博士のボーガント・デストゥパーゴなど、いまこの暗い巨きな石の建物のなかで考えていると、みんなむかし風のなつかしい青い幻燈のように思われます。では、わたくしはいつかの小さなみだしをつけながら、しずかにあの年のイーハトーヴォの五月から十月までを書きつけましょう。
+</p>
+</body>
+</html>
\ No newline at end of file
diff --git a/Android/webViewMarker/src/main/assets/css/sample.css b/Android/webViewMarker/src/main/assets/css/sample.css
new file mode 100755 (executable)
index 0000000..6b73bf8
--- /dev/null
@@ -0,0 +1,7 @@
+body {
+    margin: 0;
+    padding: 5px;
+}
+p::selection {
+    background-color: #98d9f1;
+}
\ No newline at end of file
diff --git a/Android/webViewMarker/src/main/assets/jpntext.js b/Android/webViewMarker/src/main/assets/jpntext.js
new file mode 100755 (executable)
index 0000000..72c76fe
--- /dev/null
@@ -0,0 +1,47 @@
+var jpntext = (function() {
+    var global = {
+        KIND: {
+            'mix': 0,
+            'ascii': 1,
+            'hira': 2,
+            'kata': 3,
+            'cjk': 4
+        },
+        kind: function(text) {
+            var result;
+            if (global.isAscii(text)) {
+                result = 'ascii';
+            }
+            else if (global.isHiragana(text)) {
+                result = 'hira';
+            }
+            else if (global.isKatakana(text)) {
+                result = 'kata';
+            }
+            else if (global.isKanji(text)) {
+                result = 'cjk';
+            }
+            else {
+                result = 'mix';
+            }
+            return global.KIND[result];
+        },
+        isAscii: function(text) {
+            var re = /^[\u0000-\u00ff]+$/;
+            return re.test(text);
+        },
+        isKanji: function(text) {
+            var re = /^([\u4e00-\u9fcf]|[\u3400-\u4dbf]|[\u20000-\u2a6df]|[\uf900-\ufadf])+$/;
+            return re.test(text);
+        },
+        isHiragana: function(text) {
+            var re = /^[\u3040-\u309f]+$/;
+            return re.test(text)
+        },
+        isKatakana: function(text) {
+            var re = /^[\u30a0-\u30ff]+$/;
+            return re.test(text);
+        }
+    };
+    return global;
+})();
diff --git a/Android/webViewMarker/src/main/assets/jquery-1.8.3.js b/Android/webViewMarker/src/main/assets/jquery-1.8.3.js
new file mode 100755 (executable)
index 0000000..a86bf79
--- /dev/null
@@ -0,0 +1,9472 @@
+/*!
+ * jQuery JavaScript Library v1.8.3
+ * http://jquery.com/
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: Tue Nov 13 2012 08:20:33 GMT-0500 (Eastern Standard Time)
+ */
+(function( window, undefined ) {
+var
+       // A central reference to the root jQuery(document)
+       rootjQuery,
+
+       // The deferred used on DOM ready
+       readyList,
+
+       // Use the correct document accordingly with window argument (sandbox)
+       document = window.document,
+       location = window.location,
+       navigator = window.navigator,
+
+       // Map over jQuery in case of overwrite
+       _jQuery = window.jQuery,
+
+       // Map over the $ in case of overwrite
+       _$ = window.$,
+
+       // Save a reference to some core methods
+       core_push = Array.prototype.push,
+       core_slice = Array.prototype.slice,
+       core_indexOf = Array.prototype.indexOf,
+       core_toString = Object.prototype.toString,
+       core_hasOwn = Object.prototype.hasOwnProperty,
+       core_trim = String.prototype.trim,
+
+       // Define a local copy of jQuery
+       jQuery = function( selector, context ) {
+               // The jQuery object is actually just the init constructor 'enhanced'
+               return new jQuery.fn.init( selector, context, rootjQuery );
+       },
+
+       // Used for matching numbers
+       core_pnum = /[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,
+
+       // Used for detecting and trimming whitespace
+       core_rnotwhite = /\S/,
+       core_rspace = /\s+/,
+
+       // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE)
+       rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+
+       // A simple way to check for HTML strings
+       // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+       rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
+
+       // Match a standalone tag
+       rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
+
+       // JSON RegExp
+       rvalidchars = /^[\],:{}\s]*$/,
+       rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
+       rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,
+       rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,
+
+       // Matches dashed string for camelizing
+       rmsPrefix = /^-ms-/,
+       rdashAlpha = /-([\da-z])/gi,
+
+       // Used by jQuery.camelCase as callback to replace()
+       fcamelCase = function( all, letter ) {
+               return ( letter + "" ).toUpperCase();
+       },
+
+       // The ready event handler and self cleanup method
+       DOMContentLoaded = function() {
+               if ( document.addEventListener ) {
+                       document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+                       jQuery.ready();
+               } else if ( document.readyState === "complete" ) {
+                       // we're here because readyState === "complete" in oldIE
+                       // which is good enough for us to call the dom ready!
+                       document.detachEvent( "onreadystatechange", DOMContentLoaded );
+                       jQuery.ready();
+               }
+       },
+
+       // [[Class]] -> type pairs
+       class2type = {};
+
+jQuery.fn = jQuery.prototype = {
+       constructor: jQuery,
+       init: function( selector, context, rootjQuery ) {
+               var match, elem, ret, doc;
+
+               // Handle $(""), $(null), $(undefined), $(false)
+               if ( !selector ) {
+                       return this;
+               }
+
+               // Handle $(DOMElement)
+               if ( selector.nodeType ) {
+                       this.context = this[0] = selector;
+                       this.length = 1;
+                       return this;
+               }
+
+               // Handle HTML strings
+               if ( typeof selector === "string" ) {
+                       if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
+                               // Assume that strings that start and end with <> are HTML and skip the regex check
+                               match = [ null, selector, null ];
+
+                       } else {
+                               match = rquickExpr.exec( selector );
+                       }
+
+                       // Match html or make sure no context is specified for #id
+                       if ( match && (match[1] || !context) ) {
+
+                               // HANDLE: $(html) -> $(array)
+                               if ( match[1] ) {
+                                       context = context instanceof jQuery ? context[0] : context;
+                                       doc = ( context && context.nodeType ? context.ownerDocument || context : document );
+
+                                       // scripts is true for back-compat
+                                       selector = jQuery.parseHTML( match[1], doc, true );
+                                       if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
+                                               this.attr.call( selector, context, true );
+                                       }
+
+                                       return jQuery.merge( this, selector );
+
+                               // HANDLE: $(#id)
+                               } else {
+                                       elem = document.getElementById( match[2] );
+
+                                       // Check parentNode to catch when Blackberry 4.6 returns
+                                       // nodes that are no longer in the document #6963
+                                       if ( elem && elem.parentNode ) {
+                                               // Handle the case where IE and Opera return items
+                                               // by name instead of ID
+                                               if ( elem.id !== match[2] ) {
+                                                       return rootjQuery.find( selector );
+                                               }
+
+                                               // Otherwise, we inject the element directly into the jQuery object
+                                               this.length = 1;
+                                               this[0] = elem;
+                                       }
+
+                                       this.context = document;
+                                       this.selector = selector;
+                                       return this;
+                               }
+
+                       // HANDLE: $(expr, $(...))
+                       } else if ( !context || context.jquery ) {
+                               return ( context || rootjQuery ).find( selector );
+
+                       // HANDLE: $(expr, context)
+                       // (which is just equivalent to: $(context).find(expr)
+                       } else {
+                               return this.constructor( context ).find( selector );
+                       }
+
+               // HANDLE: $(function)
+               // Shortcut for document ready
+               } else if ( jQuery.isFunction( selector ) ) {
+                       return rootjQuery.ready( selector );
+               }
+
+               if ( selector.selector !== undefined ) {
+                       this.selector = selector.selector;
+                       this.context = selector.context;
+               }
+
+               return jQuery.makeArray( selector, this );
+       },
+
+       // Start with an empty selector
+       selector: "",
+
+       // The current version of jQuery being used
+       jquery: "1.8.3",
+
+       // The default length of a jQuery object is 0
+       length: 0,
+
+       // The number of elements contained in the matched element set
+       size: function() {
+               return this.length;
+       },
+
+       toArray: function() {
+               return core_slice.call( this );
+       },
+
+       // Get the Nth element in the matched element set OR
+       // Get the whole matched element set as a clean array
+       get: function( num ) {
+               return num == null ?
+
+                       // Return a 'clean' array
+                       this.toArray() :
+
+                       // Return just the object
+                       ( num < 0 ? this[ this.length + num ] : this[ num ] );
+       },
+
+       // Take an array of elements and push it onto the stack
+       // (returning the new matched element set)
+       pushStack: function( elems, name, selector ) {
+
+               // Build a new jQuery matched element set
+               var ret = jQuery.merge( this.constructor(), elems );
+
+               // Add the old object onto the stack (as a reference)
+               ret.prevObject = this;
+
+               ret.context = this.context;
+
+               if ( name === "find" ) {
+                       ret.selector = this.selector + ( this.selector ? " " : "" ) + selector;
+               } else if ( name ) {
+                       ret.selector = this.selector + "." + name + "(" + selector + ")";
+               }
+
+               // Return the newly-formed element set
+               return ret;
+       },
+
+       // Execute a callback for every element in the matched set.
+       // (You can seed the arguments with an array of args, but this is
+       // only used internally.)
+       each: function( callback, args ) {
+               return jQuery.each( this, callback, args );
+       },
+
+       ready: function( fn ) {
+               // Add the callback
+               jQuery.ready.promise().done( fn );
+
+               return this;
+       },
+
+       eq: function( i ) {
+               i = +i;
+               return i === -1 ?
+                       this.slice( i ) :
+                       this.slice( i, i + 1 );
+       },
+
+       first: function() {
+               return this.eq( 0 );
+       },
+
+       last: function() {
+               return this.eq( -1 );
+       },
+
+       slice: function() {
+               return this.pushStack( core_slice.apply( this, arguments ),
+                       "slice", core_slice.call(arguments).join(",") );
+       },
+
+       map: function( callback ) {
+               return this.pushStack( jQuery.map(this, function( elem, i ) {
+                       return callback.call( elem, i, elem );
+               }));
+       },
+
+       end: function() {
+               return this.prevObject || this.constructor(null);
+       },
+
+       // For internal use only.
+       // Behaves like an Array's method, not like a jQuery method.
+       push: core_push,
+       sort: [].sort,
+       splice: [].splice
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+jQuery.extend = jQuery.fn.extend = function() {
+       var options, name, src, copy, copyIsArray, clone,
+               target = arguments[0] || {},
+               i = 1,
+               length = arguments.length,
+               deep = false;
+
+       // Handle a deep copy situation
+       if ( typeof target === "boolean" ) {
+               deep = target;
+               target = arguments[1] || {};
+               // skip the boolean and the target
+               i = 2;
+       }
+
+       // Handle case when target is a string or something (possible in deep copy)
+       if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+               target = {};
+       }
+
+       // extend jQuery itself if only one argument is passed
+       if ( length === i ) {
+               target = this;
+               --i;
+       }
+
+       for ( ; i < length; i++ ) {
+               // Only deal with non-null/undefined values
+               if ( (options = arguments[ i ]) != null ) {
+                       // Extend the base object
+                       for ( name in options ) {
+                               src = target[ name ];
+                               copy = options[ name ];
+
+                               // Prevent never-ending loop
+                               if ( target === copy ) {
+                                       continue;
+                               }
+
+                               // Recurse if we're merging plain objects or arrays
+                               if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+                                       if ( copyIsArray ) {
+                                               copyIsArray = false;
+                                               clone = src && jQuery.isArray(src) ? src : [];
+
+                                       } else {
+                                               clone = src && jQuery.isPlainObject(src) ? src : {};
+                                       }
+
+                                       // Never move original objects, clone them
+                                       target[ name ] = jQuery.extend( deep, clone, copy );
+
+                               // Don't bring in undefined values
+                               } else if ( copy !== undefined ) {
+                                       target[ name ] = copy;
+                               }
+                       }
+               }
+       }
+
+       // Return the modified object
+       return target;
+};
+
+jQuery.extend({
+       noConflict: function( deep ) {
+               if ( window.$ === jQuery ) {
+                       window.$ = _$;
+               }
+
+               if ( deep && window.jQuery === jQuery ) {
+                       window.jQuery = _jQuery;
+               }
+
+               return jQuery;
+       },
+
+       // Is the DOM ready to be used? Set to true once it occurs.
+       isReady: false,
+
+       // A counter to track how many items to wait for before
+       // the ready event fires. See #6781
+       readyWait: 1,
+
+       // Hold (or release) the ready event
+       holdReady: function( hold ) {
+               if ( hold ) {
+                       jQuery.readyWait++;
+               } else {
+                       jQuery.ready( true );
+               }
+       },
+
+       // Handle when the DOM is ready
+       ready: function( wait ) {
+
+               // Abort if there are pending holds or we're already ready
+               if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+                       return;
+               }
+
+               // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+               if ( !document.body ) {
+                       return setTimeout( jQuery.ready, 1 );
+               }
+
+               // Remember that the DOM is ready
+               jQuery.isReady = true;
+
+               // If a normal DOM Ready event fired, decrement, and wait if need be
+               if ( wait !== true && --jQuery.readyWait > 0 ) {
+                       return;
+               }
+
+               // If there are functions bound, to execute
+               readyList.resolveWith( document, [ jQuery ] );
+
+               // Trigger any bound ready events
+               if ( jQuery.fn.trigger ) {
+                       jQuery( document ).trigger("ready").off("ready");
+               }
+       },
+
+       // See test/unit/core.js for details concerning isFunction.
+       // Since version 1.3, DOM methods and functions like alert
+       // aren't supported. They return false on IE (#2968).
+       isFunction: function( obj ) {
+               return jQuery.type(obj) === "function";
+       },
+
+       isArray: Array.isArray || function( obj ) {
+               return jQuery.type(obj) === "array";
+       },
+
+       isWindow: function( obj ) {
+               return obj != null && obj == obj.window;
+       },
+
+       isNumeric: function( obj ) {
+               return !isNaN( parseFloat(obj) ) && isFinite( obj );
+       },
+
+       type: function( obj ) {
+               return obj == null ?
+                       String( obj ) :
+                       class2type[ core_toString.call(obj) ] || "object";
+       },
+
+       isPlainObject: function( obj ) {
+               // Must be an Object.
+               // Because of IE, we also have to check the presence of the constructor property.
+               // Make sure that DOM nodes and window objects don't pass through, as well
+               if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+                       return false;
+               }
+
+               try {
+                       // Not own constructor property must be Object
+                       if ( obj.constructor &&
+                               !core_hasOwn.call(obj, "constructor") &&
+                               !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+                               return false;
+                       }
+               } catch ( e ) {
+                       // IE8,9 Will throw exceptions on certain host objects #9897
+                       return false;
+               }
+
+               // Own properties are enumerated firstly, so to speed up,
+               // if last one is own, then all properties are own.
+
+               var key;
+               for ( key in obj ) {}
+
+               return key === undefined || core_hasOwn.call( obj, key );
+       },
+
+       isEmptyObject: function( obj ) {
+               var name;
+               for ( name in obj ) {
+                       return false;
+               }
+               return true;
+       },
+
+       error: function( msg ) {
+               throw new Error( msg );
+       },
+
+       // data: string of html
+       // context (optional): If specified, the fragment will be created in this context, defaults to document
+       // scripts (optional): If true, will include scripts passed in the html string
+       parseHTML: function( data, context, scripts ) {
+               var parsed;
+               if ( !data || typeof data !== "string" ) {
+                       return null;
+               }
+               if ( typeof context === "boolean" ) {
+                       scripts = context;
+                       context = 0;
+               }
+               context = context || document;
+
+               // Single tag
+               if ( (parsed = rsingleTag.exec( data )) ) {
+                       return [ context.createElement( parsed[1] ) ];
+               }
+
+               parsed = jQuery.buildFragment( [ data ], context, scripts ? null : [] );
+               return jQuery.merge( [],
+                       (parsed.cacheable ? jQuery.clone( parsed.fragment ) : parsed.fragment).childNodes );
+       },
+
+       parseJSON: function( data ) {
+               if ( !data || typeof data !== "string") {
+                       return null;
+               }
+
+               // Make sure leading/trailing whitespace is removed (IE can't handle it)
+               data = jQuery.trim( data );
+
+               // Attempt to parse using the native JSON parser first
+               if ( window.JSON && window.JSON.parse ) {
+                       return window.JSON.parse( data );
+               }
+
+               // Make sure the incoming data is actual JSON
+               // Logic borrowed from http://json.org/json2.js
+               if ( rvalidchars.test( data.replace( rvalidescape, "@" )
+                       .replace( rvalidtokens, "]" )
+                       .replace( rvalidbraces, "")) ) {
+
+                       return ( new Function( "return " + data ) )();
+
+               }
+               jQuery.error( "Invalid JSON: " + data );
+       },
+
+       // Cross-browser xml parsing
+       parseXML: function( data ) {
+               var xml, tmp;
+               if ( !data || typeof data !== "string" ) {
+                       return null;
+               }
+               try {
+                       if ( window.DOMParser ) { // Standard
+                               tmp = new DOMParser();
+                               xml = tmp.parseFromString( data , "text/xml" );
+                       } else { // IE
+                               xml = new ActiveXObject( "Microsoft.XMLDOM" );
+                               xml.async = "false";
+                               xml.loadXML( data );
+                       }
+               } catch( e ) {
+                       xml = undefined;
+               }
+               if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
+                       jQuery.error( "Invalid XML: " + data );
+               }
+               return xml;
+       },
+
+       noop: function() {},
+
+       // Evaluates a script in a global context
+       // Workarounds based on findings by Jim Driscoll
+       // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
+       globalEval: function( data ) {
+               if ( data && core_rnotwhite.test( data ) ) {
+                       // We use execScript on Internet Explorer
+                       // We use an anonymous function so that context is window
+                       // rather than jQuery in Firefox
+                       ( window.execScript || function( data ) {
+                               window[ "eval" ].call( window, data );
+                       } )( data );
+               }
+       },
+
+       // Convert dashed to camelCase; used by the css and data modules
+       // Microsoft forgot to hump their vendor prefix (#9572)
+       camelCase: function( string ) {
+               return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+       },
+
+       nodeName: function( elem, name ) {
+               return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+       },
+
+       // args is for internal usage only
+       each: function( obj, callback, args ) {
+               var name,
+                       i = 0,
+                       length = obj.length,
+                       isObj = length === undefined || jQuery.isFunction( obj );
+
+               if ( args ) {
+                       if ( isObj ) {
+                               for ( name in obj ) {
+                                       if ( callback.apply( obj[ name ], args ) === false ) {
+                                               break;
+                                       }
+                               }
+                       } else {
+                               for ( ; i < length; ) {
+                                       if ( callback.apply( obj[ i++ ], args ) === false ) {
+                                               break;
+                                       }
+                               }
+                       }
+
+               // A special, fast, case for the most common use of each
+               } else {
+                       if ( isObj ) {
+                               for ( name in obj ) {
+                                       if ( callback.call( obj[ name ], name, obj[ name ] ) === false ) {
+                                               break;
+                                       }
+                               }
+                       } else {
+                               for ( ; i < length; ) {
+                                       if ( callback.call( obj[ i ], i, obj[ i++ ] ) === false ) {
+                                               break;
+                                       }
+                               }
+                       }
+               }
+
+               return obj;
+       },
+
+       // Use native String.trim function wherever possible
+       trim: core_trim && !core_trim.call("\uFEFF\xA0") ?
+               function( text ) {
+                       return text == null ?
+                               "" :
+                               core_trim.call( text );
+               } :
+
+               // Otherwise use our own trimming functionality
+               function( text ) {
+                       return text == null ?
+                               "" :
+                               ( text + "" ).replace( rtrim, "" );
+               },
+
+       // results is for internal usage only
+       makeArray: function( arr, results ) {
+               var type,
+                       ret = results || [];
+
+               if ( arr != null ) {
+                       // The window, strings (and functions) also have 'length'
+                       // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
+                       type = jQuery.type( arr );
+
+                       if ( arr.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( arr ) ) {
+                               core_push.call( ret, arr );
+                       } else {
+                               jQuery.merge( ret, arr );
+                       }
+               }
+
+               return ret;
+       },
+
+       inArray: function( elem, arr, i ) {
+               var len;
+
+               if ( arr ) {
+                       if ( core_indexOf ) {
+                               return core_indexOf.call( arr, elem, i );
+                       }
+
+                       len = arr.length;
+                       i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
+
+                       for ( ; i < len; i++ ) {
+                               // Skip accessing in sparse arrays
+                               if ( i in arr && arr[ i ] === elem ) {
+                                       return i;
+                               }
+                       }
+               }
+
+               return -1;
+       },
+
+       merge: function( first, second ) {
+               var l = second.length,
+                       i = first.length,
+                       j = 0;
+
+               if ( typeof l === "number" ) {
+                       for ( ; j < l; j++ ) {
+                               first[ i++ ] = second[ j ];
+                       }
+
+               } else {
+                       while ( second[j] !== undefined ) {
+                               first[ i++ ] = second[ j++ ];
+                       }
+               }
+
+               first.length = i;
+
+               return first;
+       },
+
+       grep: function( elems, callback, inv ) {
+               var retVal,
+                       ret = [],
+                       i = 0,
+                       length = elems.length;
+               inv = !!inv;
+
+               // Go through the array, only saving the items
+               // that pass the validator function
+               for ( ; i < length; i++ ) {
+                       retVal = !!callback( elems[ i ], i );
+                       if ( inv !== retVal ) {
+                               ret.push( elems[ i ] );
+                       }
+               }
+
+               return ret;
+       },
+
+       // arg is for internal usage only
+       map: function( elems, callback, arg ) {
+               var value, key,
+                       ret = [],
+                       i = 0,
+                       length = elems.length,
+                       // jquery objects are treated as arrays
+                       isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;
+
+               // Go through the array, translating each of the items to their
+               if ( isArray ) {
+                       for ( ; i < length; i++ ) {
+                               value = callback( elems[ i ], i, arg );
+
+                               if ( value != null ) {
+                                       ret[ ret.length ] = value;
+                               }
+                       }
+
+               // Go through every key on the object,
+               } else {
+                       for ( key in elems ) {
+                               value = callback( elems[ key ], key, arg );
+
+                               if ( value != null ) {
+                                       ret[ ret.length ] = value;
+                               }
+                       }
+               }
+
+               // Flatten any nested arrays
+               return ret.concat.apply( [], ret );
+       },
+
+       // A global GUID counter for objects
+       guid: 1,
+
+       // Bind a function to a context, optionally partially applying any
+       // arguments.
+       proxy: function( fn, context ) {
+               var tmp, args, proxy;
+
+               if ( typeof context === "string" ) {
+                       tmp = fn[ context ];
+                       context = fn;
+                       fn = tmp;
+               }
+
+               // Quick check to determine if target is callable, in the spec
+               // this throws a TypeError, but we will just return undefined.
+               if ( !jQuery.isFunction( fn ) ) {
+                       return undefined;
+               }
+
+               // Simulated bind
+               args = core_slice.call( arguments, 2 );
+               proxy = function() {
+                       return fn.apply( context, args.concat( core_slice.call( arguments ) ) );
+               };
+
+               // Set the guid of unique handler to the same of original handler, so it can be removed
+               proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+               return proxy;
+       },
+
+       // Multifunctional method to get and set values of a collection
+       // The value/s can optionally be executed if it's a function
+       access: function( elems, fn, key, value, chainable, emptyGet, pass ) {
+               var exec,
+                       bulk = key == null,
+                       i = 0,
+                       length = elems.length;
+
+               // Sets many values
+               if ( key && typeof key === "object" ) {
+                       for ( i in key ) {
+                               jQuery.access( elems, fn, i, key[i], 1, emptyGet, value );
+                       }
+                       chainable = 1;
+
+               // Sets one value
+               } else if ( value !== undefined ) {
+                       // Optionally, function values get executed if exec is true
+                       exec = pass === undefined && jQuery.isFunction( value );
+
+                       if ( bulk ) {
+                               // Bulk operations only iterate when executing function values
+                               if ( exec ) {
+                                       exec = fn;
+                                       fn = function( elem, key, value ) {
+                                               return exec.call( jQuery( elem ), value );
+                                       };
+
+                               // Otherwise they run against the entire set
+                               } else {
+                                       fn.call( elems, value );
+                                       fn = null;
+                               }
+                       }
+
+                       if ( fn ) {
+                               for (; i < length; i++ ) {
+                                       fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
+                               }
+                       }
+
+                       chainable = 1;
+               }
+
+               return chainable ?
+                       elems :
+
+                       // Gets
+                       bulk ?
+                               fn.call( elems ) :
+                               length ? fn( elems[0], key ) : emptyGet;
+       },
+
+       now: function() {
+               return ( new Date() ).getTime();
+       }
+});
+
+jQuery.ready.promise = function( obj ) {
+       if ( !readyList ) {
+
+               readyList = jQuery.Deferred();
+
+               // Catch cases where $(document).ready() is called after the browser event has already occurred.
+               // we once tried to use readyState "interactive" here, but it caused issues like the one
+               // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
+               if ( document.readyState === "complete" ) {
+                       // Handle it asynchronously to allow scripts the opportunity to delay ready
+                       setTimeout( jQuery.ready, 1 );
+
+               // Standards-based browsers support DOMContentLoaded
+               } else if ( document.addEventListener ) {
+                       // Use the handy event callback
+                       document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+
+                       // A fallback to window.onload, that will always work
+                       window.addEventListener( "load", jQuery.ready, false );
+
+               // If IE event model is used
+               } else {
+                       // Ensure firing before onload, maybe late but safe also for iframes
+                       document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+                       // A fallback to window.onload, that will always work
+                       window.attachEvent( "onload", jQuery.ready );
+
+                       // If IE and not a frame
+                       // continually check to see if the document is ready
+                       var top = false;
+
+                       try {
+                               top = window.frameElement == null && document.documentElement;
+                       } catch(e) {}
+
+                       if ( top && top.doScroll ) {
+                               (function doScrollCheck() {
+                                       if ( !jQuery.isReady ) {
+
+                                               try {
+                                                       // Use the trick by Diego Perini
+                                                       // http://javascript.nwbox.com/IEContentLoaded/
+                                                       top.doScroll("left");
+                                               } catch(e) {
+                                                       return setTimeout( doScrollCheck, 50 );
+                                               }
+
+                                               // and execute any waiting functions
+                                               jQuery.ready();
+                                       }
+                               })();
+                       }
+               }
+       }
+       return readyList.promise( obj );
+};
+
+// Populate the class2type map
+jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
+       class2type[ "[object " + name + "]" ] = name.toLowerCase();
+});
+
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
+// String to Object options format cache
+var optionsCache = {};
+
+// Convert String-formatted options into Object-formatted ones and store in cache
+function createOptions( options ) {
+       var object = optionsCache[ options ] = {};
+       jQuery.each( options.split( core_rspace ), function( _, flag ) {
+               object[ flag ] = true;
+       });
+       return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ *     options: an optional list of space-separated options that will change how
+ *                     the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ *     once:                   will ensure the callback list can only be fired once (like a Deferred)
+ *
+ *     memory:                 will keep track of previous values and will call any callback added
+ *                                     after the list has been fired right away with the latest "memorized"
+ *                                     values (like a Deferred)
+ *
+ *     unique:                 will ensure a callback can only be added once (no duplicate in the list)
+ *
+ *     stopOnFalse:    interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+       // Convert options from String-formatted to Object-formatted if needed
+       // (we check in cache first)
+       options = typeof options === "string" ?
+               ( optionsCache[ options ] || createOptions( options ) ) :
+               jQuery.extend( {}, options );
+
+       var // Last fire value (for non-forgettable lists)
+               memory,
+               // Flag to know if list was already fired
+               fired,
+               // Flag to know if list is currently firing
+               firing,
+               // First callback to fire (used internally by add and fireWith)
+               firingStart,
+               // End of the loop when firing
+               firingLength,
+               // Index of currently firing callback (modified by remove if needed)
+               firingIndex,
+               // Actual callback list
+               list = [],
+               // Stack of fire calls for repeatable lists
+               stack = !options.once && [],
+               // Fire callbacks
+               fire = function( data ) {
+                       memory = options.memory && data;
+                       fired = true;
+                       firingIndex = firingStart || 0;
+                       firingStart = 0;
+                       firingLength = list.length;
+                       firing = true;
+                       for ( ; list && firingIndex < firingLength; firingIndex++ ) {
+                               if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
+                                       memory = false; // To prevent further calls using add
+                                       break;
+                               }
+                       }
+                       firing = false;
+                       if ( list ) {
+                               if ( stack ) {
+                                       if ( stack.length ) {
+                                               fire( stack.shift() );
+                                       }
+                               } else if ( memory ) {
+                                       list = [];
+                               } else {
+                                       self.disable();
+                               }
+                       }
+               },
+               // Actual Callbacks object
+               self = {
+                       // Add a callback or a collection of callbacks to the list
+                       add: function() {
+                               if ( list ) {
+                                       // First, we save the current length
+                                       var start = list.length;
+                                       (function add( args ) {
+                                               jQuery.each( args, function( _, arg ) {
+                                                       var type = jQuery.type( arg );
+                                                       if ( type === "function" ) {
+                                                               if ( !options.unique || !self.has( arg ) ) {
+                                                                       list.push( arg );
+                                                               }
+                                                       } else if ( arg && arg.length && type !== "string" ) {
+                                                               // Inspect recursively
+                                                               add( arg );
+                                                       }
+                                               });
+                                       })( arguments );
+                                       // Do we need to add the callbacks to the
+                                       // current firing batch?
+                                       if ( firing ) {
+                                               firingLength = list.length;
+                                       // With memory, if we're not firing then
+                                       // we should call right away
+                                       } else if ( memory ) {
+                                               firingStart = start;
+                                               fire( memory );
+                                       }
+                               }
+                               return this;
+                       },
+                       // Remove a callback from the list
+                       remove: function() {
+                               if ( list ) {
+                                       jQuery.each( arguments, function( _, arg ) {
+                                               var index;
+                                               while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+                                                       list.splice( index, 1 );
+                                                       // Handle firing indexes
+                                                       if ( firing ) {
+                                                               if ( index <= firingLength ) {
+                                                                       firingLength--;
+                                                               }
+                                                               if ( index <= firingIndex ) {
+                                                                       firingIndex--;
+                                                               }
+                                                       }
+                                               }
+                                       });
+                               }
+                               return this;
+                       },
+                       // Control if a given callback is in the list
+                       has: function( fn ) {
+                               return jQuery.inArray( fn, list ) > -1;
+                       },
+                       // Remove all callbacks from the list
+                       empty: function() {
+                               list = [];
+                               return this;
+                       },
+                       // Have the list do nothing anymore
+                       disable: function() {
+                               list = stack = memory = undefined;
+                               return this;
+                       },
+                       // Is it disabled?
+                       disabled: function() {
+                               return !list;
+                       },
+                       // Lock the list in its current state
+                       lock: function() {
+                               stack = undefined;
+                               if ( !memory ) {
+                                       self.disable();
+                               }
+                               return this;
+                       },
+                       // Is it locked?
+                       locked: function() {
+                               return !stack;
+                       },
+                       // Call all callbacks with the given context and arguments
+                       fireWith: function( context, args ) {
+                               args = args || [];
+                               args = [ context, args.slice ? args.slice() : args ];
+                               if ( list && ( !fired || stack ) ) {
+                                       if ( firing ) {
+                                               stack.push( args );
+                                       } else {
+                                               fire( args );
+                                       }
+                               }
+                               return this;
+                       },
+                       // Call all the callbacks with the given arguments
+                       fire: function() {
+                               self.fireWith( this, arguments );
+                               return this;
+                       },
+                       // To know if the callbacks have already been called at least once
+                       fired: function() {
+                               return !!fired;
+                       }
+               };
+
+       return self;
+};
+jQuery.extend({
+
+       Deferred: function( func ) {
+               var tuples = [
+                               // action, add listener, listener list, final state
+                               [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
+                               [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
+                               [ "notify", "progress", jQuery.Callbacks("memory") ]
+                       ],
+                       state = "pending",
+                       promise = {
+                               state: function() {
+                                       return state;
+                               },
+                               always: function() {
+                                       deferred.done( arguments ).fail( arguments );
+                                       return this;
+                               },
+                               then: function( /* fnDone, fnFail, fnProgress */ ) {
+                                       var fns = arguments;
+                                       return jQuery.Deferred(function( newDefer ) {
+                                               jQuery.each( tuples, function( i, tuple ) {
+                                                       var action = tuple[ 0 ],
+                                                               fn = fns[ i ];
+                                                       // deferred[ done | fail | progress ] for forwarding actions to newDefer
+                                                       deferred[ tuple[1] ]( jQuery.isFunction( fn ) ?
+                                                               function() {
+                                                                       var returned = fn.apply( this, arguments );
+                                                                       if ( returned && jQuery.isFunction( returned.promise ) ) {
+                                                                               returned.promise()
+                                                                                       .done( newDefer.resolve )
+                                                                                       .fail( newDefer.reject )
+                                                                                       .progress( newDefer.notify );
+                                                                       } else {
+                                                                               newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] );
+                                                                       }
+                                                               } :
+                                                               newDefer[ action ]
+                                                       );
+                                               });
+                                               fns = null;
+                                       }).promise();
+                               },
+                               // Get a promise for this deferred
+                               // If obj is provided, the promise aspect is added to the object
+                               promise: function( obj ) {
+                                       return obj != null ? jQuery.extend( obj, promise ) : promise;
+                               }
+                       },
+                       deferred = {};
+
+               // Keep pipe for back-compat
+               promise.pipe = promise.then;
+
+               // Add list-specific methods
+               jQuery.each( tuples, function( i, tuple ) {
+                       var list = tuple[ 2 ],
+                               stateString = tuple[ 3 ];
+
+                       // promise[ done | fail | progress ] = list.add
+                       promise[ tuple[1] ] = list.add;
+
+                       // Handle state
+                       if ( stateString ) {
+                               list.add(function() {
+                                       // state = [ resolved | rejected ]
+                                       state = stateString;
+
+                               // [ reject_list | resolve_list ].disable; progress_list.lock
+                               }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
+                       }
+
+                       // deferred[ resolve | reject | notify ] = list.fire
+                       deferred[ tuple[0] ] = list.fire;
+                       deferred[ tuple[0] + "With" ] = list.fireWith;
+               });
+
+               // Make the deferred a promise
+               promise.promise( deferred );
+
+               // Call given func if any
+               if ( func ) {
+                       func.call( deferred, deferred );
+               }
+
+               // All done!
+               return deferred;
+       },
+
+       // Deferred helper
+       when: function( subordinate /* , ..., subordinateN */ ) {
+               var i = 0,
+                       resolveValues = core_slice.call( arguments ),
+                       length = resolveValues.length,
+
+                       // the count of uncompleted subordinates
+                       remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
+
+                       // the master Deferred. If resolveValues consist of only a single Deferred, just use that.
+                       deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
+
+                       // Update function for both resolve and progress values
+                       updateFunc = function( i, contexts, values ) {
+                               return function( value ) {
+                                       contexts[ i ] = this;
+                                       values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;
+                                       if( values === progressValues ) {
+                                               deferred.notifyWith( contexts, values );
+                                       } else if ( !( --remaining ) ) {
+                                               deferred.resolveWith( contexts, values );
+                                       }
+                               };
+                       },
+
+                       progressValues, progressContexts, resolveContexts;
+
+               // add listeners to Deferred subordinates; treat others as resolved
+               if ( length > 1 ) {
+                       progressValues = new Array( length );
+                       progressContexts = new Array( length );
+                       resolveContexts = new Array( length );
+                       for ( ; i < length; i++ ) {
+                               if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
+                                       resolveValues[ i ].promise()
+                                               .done( updateFunc( i, resolveContexts, resolveValues ) )
+                                               .fail( deferred.reject )
+                                               .progress( updateFunc( i, progressContexts, progressValues ) );
+                               } else {
+                                       --remaining;
+                               }
+                       }
+               }
+
+               // if we're not waiting on anything, resolve the master
+               if ( !remaining ) {
+                       deferred.resolveWith( resolveContexts, resolveValues );
+               }
+
+               return deferred.promise();
+       }
+});
+jQuery.support = (function() {
+
+       var support,
+               all,
+               a,
+               select,
+               opt,
+               input,
+               fragment,
+               eventName,
+               i,
+               isSupported,
+               clickFn,
+               div = document.createElement("div");
+
+       // Setup
+       div.setAttribute( "className", "t" );
+       div.innerHTML = "  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
+
+       // Support tests won't run in some limited or non-browser environments
+       all = div.getElementsByTagName("*");
+       a = div.getElementsByTagName("a")[ 0 ];
+       if ( !all || !a || !all.length ) {
+               return {};
+       }
+
+       // First batch of tests
+       select = document.createElement("select");
+       opt = select.appendChild( document.createElement("option") );
+       input = div.getElementsByTagName("input")[ 0 ];
+
+       a.style.cssText = "top:1px;float:left;opacity:.5";
+       support = {
+               // IE strips leading whitespace when .innerHTML is used
+               leadingWhitespace: ( div.firstChild.nodeType === 3 ),
+
+               // Make sure that tbody elements aren't automatically inserted
+               // IE will insert them into empty tables
+               tbody: !div.getElementsByTagName("tbody").length,
+
+               // Make sure that link elements get serialized correctly by innerHTML
+               // This requires a wrapper element in IE
+               htmlSerialize: !!div.getElementsByTagName("link").length,
+
+               // Get the style information from getAttribute
+               // (IE uses .cssText instead)
+               style: /top/.test( a.getAttribute("style") ),
+
+               // Make sure that URLs aren't manipulated
+               // (IE normalizes it by default)
+               hrefNormalized: ( a.getAttribute("href") === "/a" ),
+
+               // Make sure that element opacity exists
+               // (IE uses filter instead)
+               // Use a regex to work around a WebKit issue. See #5145
+               opacity: /^0.5/.test( a.style.opacity ),
+
+               // Verify style float existence
+               // (IE uses styleFloat instead of cssFloat)
+               cssFloat: !!a.style.cssFloat,
+
+               // Make sure that if no value is specified for a checkbox
+               // that it defaults to "on".
+               // (WebKit defaults to "" instead)
+               checkOn: ( input.value === "on" ),
+
+               // Make sure that a selected-by-default option has a working selected property.
+               // (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+               optSelected: opt.selected,
+
+               // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
+               getSetAttribute: div.className !== "t",
+
+               // Tests for enctype support on a form (#6743)
+               enctype: !!document.createElement("form").enctype,
+
+               // Makes sure cloning an html5 element does not cause problems
+               // Where outerHTML is undefined, this still works
+               html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>",
+
+               // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode
+               boxModel: ( document.compatMode === "CSS1Compat" ),
+
+               // Will be defined later
+               submitBubbles: true,
+               changeBubbles: true,
+               focusinBubbles: false,
+               deleteExpando: true,
+               noCloneEvent: true,
+               inlineBlockNeedsLayout: false,
+               shrinkWrapBlocks: false,
+               reliableMarginRight: true,
+               boxSizingReliable: true,
+               pixelPosition: false
+       };
+
+       // Make sure checked status is properly cloned
+       input.checked = true;
+       support.noCloneChecked = input.cloneNode( true ).checked;
+
+       // Make sure that the options inside disabled selects aren't marked as disabled
+       // (WebKit marks them as disabled)
+       select.disabled = true;
+       support.optDisabled = !opt.disabled;
+
+       // Test to see if it's possible to delete an expando from an element
+       // Fails in Internet Explorer
+       try {
+               delete div.test;
+       } catch( e ) {
+               support.deleteExpando = false;
+       }
+
+       if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
+               div.attachEvent( "onclick", clickFn = function() {
+                       // Cloning a node shouldn't copy over any
+                       // bound event handlers (IE does this)
+                       support.noCloneEvent = false;
+               });
+               div.cloneNode( true ).fireEvent("onclick");
+               div.detachEvent( "onclick", clickFn );
+       }
+
+       // Check if a radio maintains its value
+       // after being appended to the DOM
+       input = document.createElement("input");
+       input.value = "t";
+       input.setAttribute( "type", "radio" );
+       support.radioValue = input.value === "t";
+
+       input.setAttribute( "checked", "checked" );
+
+       // #11217 - WebKit loses check when the name is after the checked attribute
+       input.setAttribute( "name", "t" );
+
+       div.appendChild( input );
+       fragment = document.createDocumentFragment();
+       fragment.appendChild( div.lastChild );
+
+       // WebKit doesn't clone checked state correctly in fragments
+       support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+       // Check if a disconnected checkbox will retain its checked
+       // value of true after appended to the DOM (IE6/7)
+       support.appendChecked = input.checked;
+
+       fragment.removeChild( input );
+       fragment.appendChild( div );
+
+       // Technique from Juriy Zaytsev
+       // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/
+       // We only care about the case where non-standard event systems
+       // are used, namely in IE. Short-circuiting here helps us to
+       // avoid an eval call (in setAttribute) which can cause CSP
+       // to go haywire. See: https://developer.mozilla.org/en/Security/CSP
+       if ( div.attachEvent ) {
+               for ( i in {
+                       submit: true,
+                       change: true,
+                       focusin: true
+               }) {
+                       eventName = "on" + i;
+                       isSupported = ( eventName in div );
+                       if ( !isSupported ) {
+                               div.setAttribute( eventName, "return;" );
+                               isSupported = ( typeof div[ eventName ] === "function" );
+                       }
+                       support[ i + "Bubbles" ] = isSupported;
+               }
+       }
+
+       // Run tests that need a body at doc ready
+       jQuery(function() {
+               var container, div, tds, marginDiv,
+                       divReset = "padding:0;margin:0;border:0;display:block;overflow:hidden;",
+                       body = document.getElementsByTagName("body")[0];
+
+               if ( !body ) {
+                       // Return for frameset docs that don't have a body
+                       return;
+               }
+
+               container = document.createElement("div");
+               container.style.cssText = "visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px";
+               body.insertBefore( container, body.firstChild );
+
+               // Construct the test element
+               div = document.createElement("div");
+               container.appendChild( div );
+
+               // Check if table cells still have offsetWidth/Height when they are set
+               // to display:none and there are still other visible table cells in a
+               // table row; if so, offsetWidth/Height are not reliable for use when
+               // determining if an element has been hidden directly using
+               // display:none (it is still safe to use offsets if a parent element is
+               // hidden; don safety goggles and see bug #4512 for more information).
+               // (only IE 8 fails this test)
+               div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>";
+               tds = div.getElementsByTagName("td");
+               tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none";
+               isSupported = ( tds[ 0 ].offsetHeight === 0 );
+
+               tds[ 0 ].style.display = "";
+               tds[ 1 ].style.display = "none";
+
+               // Check if empty table cells still have offsetWidth/Height
+               // (IE <= 8 fail this test)
+               support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
+
+               // Check box-sizing and margin behavior
+               div.innerHTML = "";
+               div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;";
+               support.boxSizing = ( div.offsetWidth === 4 );
+               support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 );
+
+               // NOTE: To any future maintainer, we've window.getComputedStyle
+               // because jsdom on node.js will break without it.
+               if ( window.getComputedStyle ) {
+                       support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%";
+                       support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px";
+
+                       // Check if div with explicit width and no margin-right incorrectly
+                       // gets computed margin-right based on width of container. For more
+                       // info see bug #3333
+                       // Fails in WebKit before Feb 2011 nightlies
+                       // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+                       marginDiv = document.createElement("div");
+                       marginDiv.style.cssText = div.style.cssText = divReset;
+                       marginDiv.style.marginRight = marginDiv.style.width = "0";
+                       div.style.width = "1px";
+                       div.appendChild( marginDiv );
+                       support.reliableMarginRight =
+                               !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );
+               }
+
+               if ( typeof div.style.zoom !== "undefined" ) {
+                       // Check if natively block-level elements act like inline-block
+                       // elements when setting their display to 'inline' and giving
+                       // them layout
+                       // (IE < 8 does this)
+                       div.innerHTML = "";
+                       div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1";
+                       support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 );
+
+                       // Check if elements with layout shrink-wrap their children
+                       // (IE 6 does this)
+                       div.style.display = "block";
+                       div.style.overflow = "visible";
+                       div.innerHTML = "<div></div>";
+                       div.firstChild.style.width = "5px";
+                       support.shrinkWrapBlocks = ( div.offsetWidth !== 3 );
+
+                       container.style.zoom = 1;
+               }
+
+               // Null elements to avoid leaks in IE
+               body.removeChild( container );
+               container = div = tds = marginDiv = null;
+       });
+
+       // Null elements to avoid leaks in IE
+       fragment.removeChild( div );
+       all = a = select = opt = input = fragment = div = null;
+
+       return support;
+})();
+var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
+       rmultiDash = /([A-Z])/g;
+
+jQuery.extend({
+       cache: {},
+
+       deletedIds: [],
+
+       // Remove at next major release (1.9/2.0)
+       uuid: 0,
+
+       // Unique for each copy of jQuery on the page
+       // Non-digits removed to match rinlinejQuery
+       expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
+
+       // The following elements throw uncatchable exceptions if you
+       // attempt to add expando properties to them.
+       noData: {
+               "embed": true,
+               // Ban all objects except for Flash (which handle expandos)
+               "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
+               "applet": true
+       },
+
+       hasData: function( elem ) {
+               elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
+               return !!elem && !isEmptyDataObject( elem );
+       },
+
+       data: function( elem, name, data, pvt /* Internal Use Only */ ) {
+               if ( !jQuery.acceptData( elem ) ) {
+                       return;
+               }
+
+               var thisCache, ret,
+                       internalKey = jQuery.expando,
+                       getByName = typeof name === "string",
+
+                       // We have to handle DOM nodes and JS objects differently because IE6-7
+                       // can't GC object references properly across the DOM-JS boundary
+                       isNode = elem.nodeType,
+
+                       // Only DOM nodes need the global jQuery cache; JS object data is
+                       // attached directly to the object so GC can occur automatically
+                       cache = isNode ? jQuery.cache : elem,
+
+                       // Only defining an ID for JS objects if its cache already exists allows
+                       // the code to shortcut on the same path as a DOM node with no cache
+                       id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
+
+               // Avoid doing any more work than we need to when trying to get data on an
+               // object that has no data at all
+               if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) {
+                       return;
+               }
+
+               if ( !id ) {
+                       // Only DOM nodes need a new unique ID for each element since their data
+                       // ends up in the global cache
+                       if ( isNode ) {
+                               elem[ internalKey ] = id = jQuery.deletedIds.pop() || jQuery.guid++;
+                       } else {
+                               id = internalKey;
+                       }
+               }
+
+               if ( !cache[ id ] ) {
+                       cache[ id ] = {};
+
+                       // Avoids exposing jQuery metadata on plain JS objects when the object
+                       // is serialized using JSON.stringify
+                       if ( !isNode ) {
+                               cache[ id ].toJSON = jQuery.noop;
+                       }
+               }
+
+               // An object can be passed to jQuery.data instead of a key/value pair; this gets
+               // shallow copied over onto the existing cache
+               if ( typeof name === "object" || typeof name === "function" ) {
+                       if ( pvt ) {
+                               cache[ id ] = jQuery.extend( cache[ id ], name );
+                       } else {
+                               cache[ id ].data = jQuery.extend( cache[ id ].data, name );
+                       }
+               }
+
+               thisCache = cache[ id ];
+
+               // jQuery data() is stored in a separate object inside the object's internal data
+               // cache in order to avoid key collisions between internal data and user-defined
+               // data.
+               if ( !pvt ) {
+                       if ( !thisCache.data ) {
+                               thisCache.data = {};
+                       }
+
+                       thisCache = thisCache.data;
+               }
+
+               if ( data !== undefined ) {
+                       thisCache[ jQuery.camelCase( name ) ] = data;
+               }
+
+               // Check for both converted-to-camel and non-converted data property names
+               // If a data property was specified
+               if ( getByName ) {
+
+                       // First Try to find as-is property data
+                       ret = thisCache[ name ];
+
+                       // Test for null|undefined property data
+                       if ( ret == null ) {
+
+                               // Try to find the camelCased property
+                               ret = thisCache[ jQuery.camelCase( name ) ];
+                       }
+               } else {
+                       ret = thisCache;
+               }
+
+               return ret;
+       },
+
+       removeData: function( elem, name, pvt /* Internal Use Only */ ) {
+               if ( !jQuery.acceptData( elem ) ) {
+                       return;
+               }
+
+               var thisCache, i, l,
+
+                       isNode = elem.nodeType,
+
+                       // See jQuery.data for more information
+                       cache = isNode ? jQuery.cache : elem,
+                       id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
+
+               // If there is already no cache entry for this object, there is no
+               // purpose in continuing
+               if ( !cache[ id ] ) {
+                       return;
+               }
+
+               if ( name ) {
+
+                       thisCache = pvt ? cache[ id ] : cache[ id ].data;
+
+                       if ( thisCache ) {
+
+                               // Support array or space separated string names for data keys
+                               if ( !jQuery.isArray( name ) ) {
+
+                                       // try the string as a key before any manipulation
+                                       if ( name in thisCache ) {
+                                               name = [ name ];
+                                       } else {
+
+                                               // split the camel cased version by spaces unless a key with the spaces exists
+                                               name = jQuery.camelCase( name );
+                                               if ( name in thisCache ) {
+                                                       name = [ name ];
+                                               } else {
+                                                       name = name.split(" ");
+                                               }
+                                       }
+                               }
+
+                               for ( i = 0, l = name.length; i < l; i++ ) {
+                                       delete thisCache[ name[i] ];
+                               }
+
+                               // If there is no data left in the cache, we want to continue
+                               // and let the cache object itself get destroyed
+                               if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
+                                       return;
+                               }
+                       }
+               }
+
+               // See jQuery.data for more information
+               if ( !pvt ) {
+                       delete cache[ id ].data;
+
+                       // Don't destroy the parent cache unless the internal data object
+                       // had been the only thing left in it
+                       if ( !isEmptyDataObject( cache[ id ] ) ) {
+                               return;
+                       }
+               }
+
+               // Destroy the cache
+               if ( isNode ) {
+                       jQuery.cleanData( [ elem ], true );
+
+               // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)
+               } else if ( jQuery.support.deleteExpando || cache != cache.window ) {
+                       delete cache[ id ];
+
+               // When all else fails, null
+               } else {
+                       cache[ id ] = null;
+               }
+       },
+
+       // For internal use only.
+       _data: function( elem, name, data ) {
+               return jQuery.data( elem, name, data, true );
+       },
+
+       // A method for determining if a DOM node can handle the data expando
+       acceptData: function( elem ) {
+               var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
+
+               // nodes accept data unless otherwise specified; rejection can be conditional
+               return !noData || noData !== true && elem.getAttribute("classid") === noData;
+       }
+});
+
+jQuery.fn.extend({
+       data: function( key, value ) {
+               var parts, part, attr, name, l,
+                       elem = this[0],
+                       i = 0,
+                       data = null;
+
+               // Gets all values
+               if ( key === undefined ) {
+                       if ( this.length ) {
+                               data = jQuery.data( elem );
+
+                               if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
+                                       attr = elem.attributes;
+                                       for ( l = attr.length; i < l; i++ ) {
+                                               name = attr[i].name;
+
+                                               if ( !name.indexOf( "data-" ) ) {
+                                                       name = jQuery.camelCase( name.substring(5) );
+
+                                                       dataAttr( elem, name, data[ name ] );
+                                               }
+                                       }
+                                       jQuery._data( elem, "parsedAttrs", true );
+                               }
+                       }
+
+                       return data;
+               }
+
+               // Sets multiple values
+               if ( typeof key === "object" ) {
+                       return this.each(function() {
+                               jQuery.data( this, key );
+                       });
+               }
+
+               parts = key.split( ".", 2 );
+               parts[1] = parts[1] ? "." + parts[1] : "";
+               part = parts[1] + "!";
+
+               return jQuery.access( this, function( value ) {
+
+                       if ( value === undefined ) {
+                               data = this.triggerHandler( "getData" + part, [ parts[0] ] );
+
+                               // Try to fetch any internally stored data first
+                               if ( data === undefined && elem ) {
+                                       data = jQuery.data( elem, key );
+                                       data = dataAttr( elem, key, data );
+                               }
+
+                               return data === undefined && parts[1] ?
+                                       this.data( parts[0] ) :
+                                       data;
+                       }
+
+                       parts[1] = value;
+                       this.each(function() {
+                               var self = jQuery( this );
+
+                               self.triggerHandler( "setData" + part, parts );
+                               jQuery.data( this, key, value );
+                               self.triggerHandler( "changeData" + part, parts );
+                       });
+               }, null, value, arguments.length > 1, null, false );
+       },
+
+       removeData: function( key ) {
+               return this.each(function() {
+                       jQuery.removeData( this, key );
+               });
+       }
+});
+
+function dataAttr( elem, key, data ) {
+       // If nothing was found internally, try to fetch any
+       // data from the HTML5 data-* attribute
+       if ( data === undefined && elem.nodeType === 1 ) {
+
+               var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
+
+               data = elem.getAttribute( name );
+
+               if ( typeof data === "string" ) {
+                       try {
+                               data = data === "true" ? true :
+                               data === "false" ? false :
+                               data === "null" ? null :
+                               // Only convert to a number if it doesn't change the string
+                               +data + "" === data ? +data :
+                               rbrace.test( data ) ? jQuery.parseJSON( data ) :
+                                       data;
+                       } catch( e ) {}
+
+                       // Make sure we set the data so it isn't changed later
+                       jQuery.data( elem, key, data );
+
+               } else {
+                       data = undefined;
+               }
+       }
+
+       return data;
+}
+
+// checks a cache object for emptiness
+function isEmptyDataObject( obj ) {
+       var name;
+       for ( name in obj ) {
+
+               // if the public data object is empty, the private is still empty
+               if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
+                       continue;
+               }
+               if ( name !== "toJSON" ) {
+                       return false;
+               }
+       }
+
+       return true;
+}
+jQuery.extend({
+       queue: function( elem, type, data ) {
+               var queue;
+
+               if ( elem ) {
+                       type = ( type || "fx" ) + "queue";
+                       queue = jQuery._data( elem, type );
+
+                       // Speed up dequeue by getting out quickly if this is just a lookup
+                       if ( data ) {
+                               if ( !queue || jQuery.isArray(data) ) {
+                                       queue = jQuery._data( elem, type, jQuery.makeArray(data) );
+                               } else {
+                                       queue.push( data );
+                               }
+                       }
+                       return queue || [];
+               }
+       },
+
+       dequeue: function( elem, type ) {
+               type = type || "fx";
+
+               var queue = jQuery.queue( elem, type ),
+                       startLength = queue.length,
+                       fn = queue.shift(),
+                       hooks = jQuery._queueHooks( elem, type ),
+                       next = function() {
+                               jQuery.dequeue( elem, type );
+                       };
+
+               // If the fx queue is dequeued, always remove the progress sentinel
+               if ( fn === "inprogress" ) {
+                       fn = queue.shift();
+                       startLength--;
+               }
+
+               if ( fn ) {
+
+                       // Add a progress sentinel to prevent the fx queue from being
+                       // automatically dequeued
+                       if ( type === "fx" ) {
+                               queue.unshift( "inprogress" );
+                       }
+
+                       // clear up the last queue stop function
+                       delete hooks.stop;
+                       fn.call( elem, next, hooks );
+               }
+
+               if ( !startLength && hooks ) {
+                       hooks.empty.fire();
+               }
+       },
+
+       // not intended for public consumption - generates a queueHooks object, or returns the current one
+       _queueHooks: function( elem, type ) {
+               var key = type + "queueHooks";
+               return jQuery._data( elem, key ) || jQuery._data( elem, key, {
+                       empty: jQuery.Callbacks("once memory").add(function() {
+                               jQuery.removeData( elem, type + "queue", true );
+                               jQuery.removeData( elem, key, true );
+                       })
+               });
+       }
+});
+
+jQuery.fn.extend({
+       queue: function( type, data ) {
+               var setter = 2;
+
+               if ( typeof type !== "string" ) {
+                       data = type;
+                       type = "fx";
+                       setter--;
+               }
+
+               if ( arguments.length < setter ) {
+                       return jQuery.queue( this[0], type );
+               }
+
+               return data === undefined ?
+                       this :
+                       this.each(function() {
+                               var queue = jQuery.queue( this, type, data );
+
+                               // ensure a hooks for this queue
+                               jQuery._queueHooks( this, type );
+
+                               if ( type === "fx" && queue[0] !== "inprogress" ) {
+                                       jQuery.dequeue( this, type );
+                               }
+                       });
+       },
+       dequeue: function( type ) {
+               return this.each(function() {
+                       jQuery.dequeue( this, type );
+               });
+       },
+       // Based off of the plugin by Clint Helfers, with permission.
+       // http://blindsignals.com/index.php/2009/07/jquery-delay/
+       delay: function( time, type ) {
+               time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+               type = type || "fx";
+
+               return this.queue( type, function( next, hooks ) {
+                       var timeout = setTimeout( next, time );
+                       hooks.stop = function() {
+                               clearTimeout( timeout );
+                       };
+               });
+       },
+       clearQueue: function( type ) {
+               return this.queue( type || "fx", [] );
+       },
+       // Get a promise resolved when queues of a certain type
+       // are emptied (fx is the type by default)
+       promise: function( type, obj ) {
+               var tmp,
+                       count = 1,
+                       defer = jQuery.Deferred(),
+                       elements = this,
+                       i = this.length,
+                       resolve = function() {
+                               if ( !( --count ) ) {
+                                       defer.resolveWith( elements, [ elements ] );
+                               }
+                       };
+
+               if ( typeof type !== "string" ) {
+                       obj = type;
+                       type = undefined;
+               }
+               type = type || "fx";
+
+               while( i-- ) {
+                       tmp = jQuery._data( elements[ i ], type + "queueHooks" );
+                       if ( tmp && tmp.empty ) {
+                               count++;
+                               tmp.empty.add( resolve );
+                       }
+               }
+               resolve();
+               return defer.promise( obj );
+       }
+});
+var nodeHook, boolHook, fixSpecified,
+       rclass = /[\t\r\n]/g,
+       rreturn = /\r/g,
+       rtype = /^(?:button|input)$/i,
+       rfocusable = /^(?:button|input|object|select|textarea)$/i,
+       rclickable = /^a(?:rea|)$/i,
+       rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
+       getSetAttribute = jQuery.support.getSetAttribute;
+
+jQuery.fn.extend({
+       attr: function( name, value ) {
+               return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );
+       },
+
+       removeAttr: function( name ) {
+               return this.each(function() {
+                       jQuery.removeAttr( this, name );
+               });
+       },
+
+       prop: function( name, value ) {
+               return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );
+       },
+
+       removeProp: function( name ) {
+               name = jQuery.propFix[ name ] || name;
+               return this.each(function() {
+                       // try/catch handles cases where IE balks (such as removing a property on window)
+                       try {
+                               this[ name ] = undefined;
+                               delete this[ name ];
+                       } catch( e ) {}
+               });
+       },
+
+       addClass: function( value ) {
+               var classNames, i, l, elem,
+                       setClass, c, cl;
+
+               if ( jQuery.isFunction( value ) ) {
+                       return this.each(function( j ) {
+                               jQuery( this ).addClass( value.call(this, j, this.className) );
+                       });
+               }
+
+               if ( value && typeof value === "string" ) {
+                       classNames = value.split( core_rspace );
+
+                       for ( i = 0, l = this.length; i < l; i++ ) {
+                               elem = this[ i ];
+
+                               if ( elem.nodeType === 1 ) {
+                                       if ( !elem.className && classNames.length === 1 ) {
+                                               elem.className = value;
+
+                                       } else {
+                                               setClass = " " + elem.className + " ";
+
+                                               for ( c = 0, cl = classNames.length; c < cl; c++ ) {
+                                                       if ( setClass.indexOf( " " + classNames[ c ] + " " ) < 0 ) {
+                                                               setClass += classNames[ c ] + " ";
+                                                       }
+                                               }
+                                               elem.className = jQuery.trim( setClass );
+                                       }
+                               }
+                       }
+               }
+
+               return this;
+       },
+
+       removeClass: function( value ) {
+               var removes, className, elem, c, cl, i, l;
+
+               if ( jQuery.isFunction( value ) ) {
+                       return this.each(function( j ) {
+                               jQuery( this ).removeClass( value.call(this, j, this.className) );
+                       });
+               }
+               if ( (value && typeof value === "string") || value === undefined ) {
+                       removes = ( value || "" ).split( core_rspace );
+
+                       for ( i = 0, l = this.length; i < l; i++ ) {
+                               elem = this[ i ];
+                               if ( elem.nodeType === 1 && elem.className ) {
+
+                                       className = (" " + elem.className + " ").replace( rclass, " " );
+
+                                       // loop over each item in the removal list
+                                       for ( c = 0, cl = removes.length; c < cl; c++ ) {
+                                               // Remove until there is nothing to remove,
+                                               while ( className.indexOf(" " + removes[ c ] + " ") >= 0 ) {
+                                                       className = className.replace( " " + removes[ c ] + " " , " " );
+                                               }
+                                       }
+                                       elem.className = value ? jQuery.trim( className ) : "";
+                               }
+                       }
+               }
+
+               return this;
+       },
+
+       toggleClass: function( value, stateVal ) {
+               var type = typeof value,
+                       isBool = typeof stateVal === "boolean";
+
+               if ( jQuery.isFunction( value ) ) {
+                       return this.each(function( i ) {
+                               jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
+                       });
+               }
+
+               return this.each(function() {
+                       if ( type === "string" ) {
+                               // toggle individual class names
+                               var className,
+                                       i = 0,
+                                       self = jQuery( this ),
+                                       state = stateVal,
+                                       classNames = value.split( core_rspace );
+
+                               while ( (className = classNames[ i++ ]) ) {
+                                       // check each className given, space separated list
+                                       state = isBool ? state : !self.hasClass( className );
+                                       self[ state ? "addClass" : "removeClass" ]( className );
+                               }
+
+                       } else if ( type === "undefined" || type === "boolean" ) {
+                               if ( this.className ) {
+                                       // store className if set
+                                       jQuery._data( this, "__className__", this.className );
+                               }
+
+                               // toggle whole className
+                               this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
+                       }
+               });
+       },
+
+       hasClass: function( selector ) {
+               var className = " " + selector + " ",
+                       i = 0,
+                       l = this.length;
+               for ( ; i < l; i++ ) {
+                       if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       },
+
+       val: function( value ) {
+               var hooks, ret, isFunction,
+                       elem = this[0];
+
+               if ( !arguments.length ) {
+                       if ( elem ) {
+                               hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+                               if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
+                                       return ret;
+                               }
+
+                               ret = elem.value;
+
+                               return typeof ret === "string" ?
+                                       // handle most common string cases
+                                       ret.replace(rreturn, "") :
+                                       // handle cases where value is null/undef or number
+                                       ret == null ? "" : ret;
+                       }
+
+                       return;
+               }
+
+               isFunction = jQuery.isFunction( value );
+
+               return this.each(function( i ) {
+                       var val,
+                               self = jQuery(this);
+
+                       if ( this.nodeType !== 1 ) {
+                               return;
+                       }
+
+                       if ( isFunction ) {
+                               val = value.call( this, i, self.val() );
+                       } else {
+                               val = value;
+                       }
+
+                       // Treat null/undefined as ""; convert numbers to string
+                       if ( val == null ) {
+                               val = "";
+                       } else if ( typeof val === "number" ) {
+                               val += "";
+                       } else if ( jQuery.isArray( val ) ) {
+                               val = jQuery.map(val, function ( value ) {
+                                       return value == null ? "" : value + "";
+                               });
+                       }
+
+                       hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+                       // If set returns undefined, fall back to normal setting
+                       if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
+                               this.value = val;
+                       }
+               });
+       }
+});
+
+jQuery.extend({
+       valHooks: {
+               option: {
+                       get: function( elem ) {
+                               // attributes.value is undefined in Blackberry 4.7 but
+                               // uses .value. See #6932
+                               var val = elem.attributes.value;
+                               return !val || val.specified ? elem.value : elem.text;
+                       }
+               },
+               select: {
+                       get: function( elem ) {
+                               var value, option,
+                                       options = elem.options,
+                                       index = elem.selectedIndex,
+                                       one = elem.type === "select-one" || index < 0,
+                                       values = one ? null : [],
+                                       max = one ? index + 1 : options.length,
+                                       i = index < 0 ?
+                                               max :
+                                               one ? index : 0;
+
+                               // Loop through all the selected options
+                               for ( ; i < max; i++ ) {
+                                       option = options[ i ];
+
+                                       // oldIE doesn't update selected after form reset (#2551)
+                                       if ( ( option.selected || i === index ) &&
+                                                       // Don't return options that are disabled or in a disabled optgroup
+                                                       ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) &&
+                                                       ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
+
+                                               // Get the specific value for the option
+                                               value = jQuery( option ).val();
+
+                                               // We don't need an array for one selects
+                                               if ( one ) {
+                                                       return value;
+                                               }
+
+                                               // Multi-Selects return an array
+                                               values.push( value );
+                                       }
+                               }
+
+                               return values;
+                       },
+
+                       set: function( elem, value ) {
+                               var values = jQuery.makeArray( value );
+
+                               jQuery(elem).find("option").each(function() {
+                                       this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
+                               });
+
+                               if ( !values.length ) {
+                                       elem.selectedIndex = -1;
+                               }
+                               return values;
+                       }
+               }
+       },
+
+       // Unused in 1.8, left in so attrFn-stabbers won't die; remove in 1.9
+       attrFn: {},
+
+       attr: function( elem, name, value, pass ) {
+               var ret, hooks, notxml,
+                       nType = elem.nodeType;
+
+               // don't get/set attributes on text, comment and attribute nodes
+               if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+                       return;
+               }
+
+               if ( pass && jQuery.isFunction( jQuery.fn[ name ] ) ) {
+                       return jQuery( elem )[ name ]( value );
+               }
+
+               // Fallback to prop when attributes are not supported
+               if ( typeof elem.getAttribute === "undefined" ) {
+                       return jQuery.prop( elem, name, value );
+               }
+
+               notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+               // All attributes are lowercase
+               // Grab necessary hook if one is defined
+               if ( notxml ) {
+                       name = name.toLowerCase();
+                       hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );
+               }
+
+               if ( value !== undefined ) {
+
+                       if ( value === null ) {
+                               jQuery.removeAttr( elem, name );
+                               return;
+
+                       } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) {
+                               return ret;
+
+                       } else {
+                               elem.setAttribute( name, value + "" );
+                               return value;
+                       }
+
+               } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) {
+                       return ret;
+
+               } else {
+
+                       ret = elem.getAttribute( name );
+
+                       // Non-existent attributes return null, we normalize to undefined
+                       return ret === null ?
+                               undefined :
+                               ret;
+               }
+       },
+
+       removeAttr: function( elem, value ) {
+               var propName, attrNames, name, isBool,
+                       i = 0;
+
+               if ( value && elem.nodeType === 1 ) {
+
+                       attrNames = value.split( core_rspace );
+
+                       for ( ; i < attrNames.length; i++ ) {
+                               name = attrNames[ i ];
+
+                               if ( name ) {
+                                       propName = jQuery.propFix[ name ] || name;
+                                       isBool = rboolean.test( name );
+
+                                       // See #9699 for explanation of this approach (setting first, then removal)
+                                       // Do not do this for boolean attributes (see #10870)
+                                       if ( !isBool ) {
+                                               jQuery.attr( elem, name, "" );
+                                       }
+                                       elem.removeAttribute( getSetAttribute ? name : propName );
+
+                                       // Set corresponding property to false for boolean attributes
+                                       if ( isBool && propName in elem ) {
+                                               elem[ propName ] = false;
+                                       }
+                               }
+                       }
+               }
+       },
+
+       attrHooks: {
+               type: {
+                       set: function( elem, value ) {
+                               // We can't allow the type property to be changed (since it causes problems in IE)
+                               if ( rtype.test( elem.nodeName ) && elem.parentNode ) {
+                                       jQuery.error( "type property can't be changed" );
+                               } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
+                                       // Setting the type on a radio button after the value resets the value in IE6-9
+                                       // Reset value to it's default in case type is set after value
+                                       // This is for element creation
+                                       var val = elem.value;
+                                       elem.setAttribute( "type", value );
+                                       if ( val ) {
+                                               elem.value = val;
+                                       }
+                                       return value;
+                               }
+                       }
+               },
+               // Use the value property for back compat
+               // Use the nodeHook for button elements in IE6/7 (#1954)
+               value: {
+                       get: function( elem, name ) {
+                               if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
+                                       return nodeHook.get( elem, name );
+                               }
+                               return name in elem ?
+                                       elem.value :
+                                       null;
+                       },
+                       set: function( elem, value, name ) {
+                               if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
+                                       return nodeHook.set( elem, value, name );
+                               }
+                               // Does not return so that setAttribute is also used
+                               elem.value = value;
+                       }
+               }
+       },
+
+       propFix: {
+               tabindex: "tabIndex",
+               readonly: "readOnly",
+               "for": "htmlFor",
+               "class": "className",
+               maxlength: "maxLength",
+               cellspacing: "cellSpacing",
+               cellpadding: "cellPadding",
+               rowspan: "rowSpan",
+               colspan: "colSpan",
+               usemap: "useMap",
+               frameborder: "frameBorder",
+               contenteditable: "contentEditable"
+       },
+
+       prop: function( elem, name, value ) {
+               var ret, hooks, notxml,
+                       nType = elem.nodeType;
+
+               // don't get/set properties on text, comment and attribute nodes
+               if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+                       return;
+               }
+
+               notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+               if ( notxml ) {
+                       // Fix name and attach hooks
+                       name = jQuery.propFix[ name ] || name;
+                       hooks = jQuery.propHooks[ name ];
+               }
+
+               if ( value !== undefined ) {
+                       if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
+                               return ret;
+
+                       } else {
+                               return ( elem[ name ] = value );
+                       }
+
+               } else {
+                       if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
+                               return ret;
+
+                       } else {
+                               return elem[ name ];
+                       }
+               }
+       },
+
+       propHooks: {
+               tabIndex: {
+                       get: function( elem ) {
+                               // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+                               // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+                               var attributeNode = elem.getAttributeNode("tabindex");
+
+                               return attributeNode && attributeNode.specified ?
+                                       parseInt( attributeNode.value, 10 ) :
+                                       rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+                                               0 :
+                                               undefined;
+                       }
+               }
+       }
+});
+
+// Hook for boolean attributes
+boolHook = {
+       get: function( elem, name ) {
+               // Align boolean attributes with corresponding properties
+               // Fall back to attribute presence where some booleans are not supported
+               var attrNode,
+                       property = jQuery.prop( elem, name );
+               return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ?
+                       name.toLowerCase() :
+                       undefined;
+       },
+       set: function( elem, value, name ) {
+               var propName;
+               if ( value === false ) {
+                       // Remove boolean attributes when set to false
+                       jQuery.removeAttr( elem, name );
+               } else {
+                       // value is true since we know at this point it's type boolean and not false
+                       // Set boolean attributes to the same name and set the DOM property
+                       propName = jQuery.propFix[ name ] || name;
+                       if ( propName in elem ) {
+                               // Only set the IDL specifically if it already exists on the element
+                               elem[ propName ] = true;
+                       }
+
+                       elem.setAttribute( name, name.toLowerCase() );
+               }
+               return name;
+       }
+};
+
+// IE6/7 do not support getting/setting some attributes with get/setAttribute
+if ( !getSetAttribute ) {
+
+       fixSpecified = {
+               name: true,
+               id: true,
+               coords: true
+       };
+
+       // Use this for any attribute in IE6/7
+       // This fixes almost every IE6/7 issue
+       nodeHook = jQuery.valHooks.button = {
+               get: function( elem, name ) {
+                       var ret;
+                       ret = elem.getAttributeNode( name );
+                       return ret && ( fixSpecified[ name ] ? ret.value !== "" : ret.specified ) ?
+                               ret.value :
+                               undefined;
+               },
+               set: function( elem, value, name ) {
+                       // Set the existing or create a new attribute node
+                       var ret = elem.getAttributeNode( name );
+                       if ( !ret ) {
+                               ret = document.createAttribute( name );
+                               elem.setAttributeNode( ret );
+                       }
+                       return ( ret.value = value + "" );
+               }
+       };
+
+       // Set width and height to auto instead of 0 on empty string( Bug #8150 )
+       // This is for removals
+       jQuery.each([ "width", "height" ], function( i, name ) {
+               jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+                       set: function( elem, value ) {
+                               if ( value === "" ) {
+                                       elem.setAttribute( name, "auto" );
+                                       return value;
+                               }
+                       }
+               });
+       });
+
+       // Set contenteditable to false on removals(#10429)
+       // Setting to empty string throws an error as an invalid value
+       jQuery.attrHooks.contenteditable = {
+               get: nodeHook.get,
+               set: function( elem, value, name ) {
+                       if ( value === "" ) {
+                               value = "false";
+                       }
+                       nodeHook.set( elem, value, name );
+               }
+       };
+}
+
+
+// Some attributes require a special call on IE
+if ( !jQuery.support.hrefNormalized ) {
+       jQuery.each([ "href", "src", "width", "height" ], function( i, name ) {
+               jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+                       get: function( elem ) {
+                               var ret = elem.getAttribute( name, 2 );
+                               return ret === null ? undefined : ret;
+                       }
+               });
+       });
+}
+
+if ( !jQuery.support.style ) {
+       jQuery.attrHooks.style = {
+               get: function( elem ) {
+                       // Return undefined in the case of empty string
+                       // Normalize to lowercase since IE uppercases css property names
+                       return elem.style.cssText.toLowerCase() || undefined;
+               },
+               set: function( elem, value ) {
+                       return ( elem.style.cssText = value + "" );
+               }
+       };
+}
+
+// Safari mis-reports the default selected property of an option
+// Accessing the parent's selectedIndex property fixes it
+if ( !jQuery.support.optSelected ) {
+       jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, {
+               get: function( elem ) {
+                       var parent = elem.parentNode;
+
+                       if ( parent ) {
+                               parent.selectedIndex;
+
+                               // Make sure that it also works with optgroups, see #5701
+                               if ( parent.parentNode ) {
+                                       parent.parentNode.selectedIndex;
+                               }
+                       }
+                       return null;
+               }
+       });
+}
+
+// IE6/7 call enctype encoding
+if ( !jQuery.support.enctype ) {
+       jQuery.propFix.enctype = "encoding";
+}
+
+// Radios and checkboxes getter/setter
+if ( !jQuery.support.checkOn ) {
+       jQuery.each([ "radio", "checkbox" ], function() {
+               jQuery.valHooks[ this ] = {
+                       get: function( elem ) {
+                               // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
+                               return elem.getAttribute("value") === null ? "on" : elem.value;
+                       }
+               };
+       });
+}
+jQuery.each([ "radio", "checkbox" ], function() {
+       jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], {
+               set: function( elem, value ) {
+                       if ( jQuery.isArray( value ) ) {
+                               return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
+                       }
+               }
+       });
+});
+var rformElems = /^(?:textarea|input|select)$/i,
+       rtypenamespace = /^([^\.]*|)(?:\.(.+)|)$/,
+       rhoverHack = /(?:^|\s)hover(\.\S+|)\b/,
+       rkeyEvent = /^key/,
+       rmouseEvent = /^(?:mouse|contextmenu)|click/,
+       rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+       hoverHack = function( events ) {
+               return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" );
+       };
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+       add: function( elem, types, handler, data, selector ) {
+
+               var elemData, eventHandle, events,
+                       t, tns, type, namespaces, handleObj,
+                       handleObjIn, handlers, special;
+
+               // Don't attach events to noData or text/comment nodes (allow plain objects tho)
+               if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) {
+                       return;
+               }
+
+               // Caller can pass in an object of custom data in lieu of the handler
+               if ( handler.handler ) {
+                       handleObjIn = handler;
+                       handler = handleObjIn.handler;
+                       selector = handleObjIn.selector;
+               }
+
+               // Make sure that the handler has a unique ID, used to find/remove it later
+               if ( !handler.guid ) {
+                       handler.guid = jQuery.guid++;
+               }
+
+               // Init the element's event structure and main handler, if this is the first
+               events = elemData.events;
+               if ( !events ) {
+                       elemData.events = events = {};
+               }
+               eventHandle = elemData.handle;
+               if ( !eventHandle ) {
+                       elemData.handle = eventHandle = function( e ) {
+                               // Discard the second event of a jQuery.event.trigger() and
+                               // when an event is called after a page has unloaded
+                               return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
+                                       jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
+                                       undefined;
+                       };
+                       // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
+                       eventHandle.elem = elem;
+               }
+
+               // Handle multiple events separated by a space
+               // jQuery(...).bind("mouseover mouseout", fn);
+               types = jQuery.trim( hoverHack(types) ).split( " " );
+               for ( t = 0; t < types.length; t++ ) {
+
+                       tns = rtypenamespace.exec( types[t] ) || [];
+                       type = tns[1];
+                       namespaces = ( tns[2] || "" ).split( "." ).sort();
+
+                       // If event changes its type, use the special event handlers for the changed type
+                       special = jQuery.event.special[ type ] || {};
+
+                       // If selector defined, determine special event api type, otherwise given type
+                       type = ( selector ? special.delegateType : special.bindType ) || type;
+
+                       // Update special based on newly reset type
+                       special = jQuery.event.special[ type ] || {};
+
+                       // handleObj is passed to all event handlers
+                       handleObj = jQuery.extend({
+                               type: type,
+                               origType: tns[1],
+                               data: data,
+                               handler: handler,
+                               guid: handler.guid,
+                               selector: selector,
+                               needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+                               namespace: namespaces.join(".")
+                       }, handleObjIn );
+
+                       // Init the event handler queue if we're the first
+                       handlers = events[ type ];
+                       if ( !handlers ) {
+                               handlers = events[ type ] = [];
+                               handlers.delegateCount = 0;
+
+                               // Only use addEventListener/attachEvent if the special events handler returns false
+                               if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+                                       // Bind the global event handler to the element
+                                       if ( elem.addEventListener ) {
+                                               elem.addEventListener( type, eventHandle, false );
+
+                                       } else if ( elem.attachEvent ) {
+                                               elem.attachEvent( "on" + type, eventHandle );
+                                       }
+                               }
+                       }
+
+                       if ( special.add ) {
+                               special.add.call( elem, handleObj );
+
+                               if ( !handleObj.handler.guid ) {
+                                       handleObj.handler.guid = handler.guid;
+                               }
+                       }
+
+                       // Add to the element's handler list, delegates in front
+                       if ( selector ) {
+                               handlers.splice( handlers.delegateCount++, 0, handleObj );
+                       } else {
+                               handlers.push( handleObj );
+                       }
+
+                       // Keep track of which events have ever been used, for event optimization
+                       jQuery.event.global[ type ] = true;
+               }
+
+               // Nullify elem to prevent memory leaks in IE
+               elem = null;
+       },
+
+       global: {},
+
+       // Detach an event or set of events from an element
+       remove: function( elem, types, handler, selector, mappedTypes ) {
+
+               var t, tns, type, origType, namespaces, origCount,
+                       j, events, special, eventType, handleObj,
+                       elemData = jQuery.hasData( elem ) && jQuery._data( elem );
+
+               if ( !elemData || !(events = elemData.events) ) {
+                       return;
+               }
+
+               // Once for each type.namespace in types; type may be omitted
+               types = jQuery.trim( hoverHack( types || "" ) ).split(" ");
+               for ( t = 0; t < types.length; t++ ) {
+                       tns = rtypenamespace.exec( types[t] ) || [];
+                       type = origType = tns[1];
+                       namespaces = tns[2];
+
+                       // Unbind all events (on this namespace, if provided) for the element
+                       if ( !type ) {
+                               for ( type in events ) {
+                                       jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+                               }
+                               continue;
+                       }
+
+                       special = jQuery.event.special[ type ] || {};
+                       type = ( selector? special.delegateType : special.bindType ) || type;
+                       eventType = events[ type ] || [];
+                       origCount = eventType.length;
+                       namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.|)") + "(\\.|$)") : null;
+
+                       // Remove matching events
+                       for ( j = 0; j < eventType.length; j++ ) {
+                               handleObj = eventType[ j ];
+
+                               if ( ( mappedTypes || origType === handleObj.origType ) &&
+                                        ( !handler || handler.guid === handleObj.guid ) &&
+                                        ( !namespaces || namespaces.test( handleObj.namespace ) ) &&
+                                        ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
+                                       eventType.splice( j--, 1 );
+
+                                       if ( handleObj.selector ) {
+                                               eventType.delegateCount--;
+                                       }
+                                       if ( special.remove ) {
+                                               special.remove.call( elem, handleObj );
+                                       }
+                               }
+                       }
+
+                       // Remove generic event handler if we removed something and no more handlers exist
+                       // (avoids potential for endless recursion during removal of special event handlers)
+                       if ( eventType.length === 0 && origCount !== eventType.length ) {
+                               if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+                                       jQuery.removeEvent( elem, type, elemData.handle );
+                               }
+
+                               delete events[ type ];
+                       }
+               }
+
+               // Remove the expando if it's no longer used
+               if ( jQuery.isEmptyObject( events ) ) {
+                       delete elemData.handle;
+
+                       // removeData also checks for emptiness and clears the expando if empty
+                       // so use it instead of delete
+                       jQuery.removeData( elem, "events", true );
+               }
+       },
+
+       // Events that are safe to short-circuit if no handlers are attached.
+       // Native DOM events should not be added, they may have inline handlers.
+       customEvent: {
+               "getData": true,
+               "setData": true,
+               "changeData": true
+       },
+
+       trigger: function( event, data, elem, onlyHandlers ) {
+               // Don't do events on text and comment nodes
+               if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) {
+                       return;
+               }
+
+               // Event object or event type
+               var cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType,
+                       type = event.type || event,
+                       namespaces = [];
+
+               // focus/blur morphs to focusin/out; ensure we're not firing them right now
+               if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+                       return;
+               }
+
+               if ( type.indexOf( "!" ) >= 0 ) {
+                       // Exclusive events trigger only for the exact event (no namespaces)
+                       type = type.slice(0, -1);
+                       exclusive = true;
+               }
+
+               if ( type.indexOf( "." ) >= 0 ) {
+                       // Namespaced trigger; create a regexp to match event type in handle()
+                       namespaces = type.split(".");
+                       type = namespaces.shift();
+                       namespaces.sort();
+               }
+
+               if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) {
+                       // No jQuery handlers for this event type, and it can't have inline handlers
+                       return;
+               }
+
+               // Caller can pass in an Event, Object, or just an event type string
+               event = typeof event === "object" ?
+                       // jQuery.Event object
+                       event[ jQuery.expando ] ? event :
+                       // Object literal
+                       new jQuery.Event( type, event ) :
+                       // Just the event type (string)
+                       new jQuery.Event( type );
+
+               event.type = type;
+               event.isTrigger = true;
+               event.exclusive = exclusive;
+               event.namespace = namespaces.join( "." );
+               event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") : null;
+               ontype = type.indexOf( ":" ) < 0 ? "on" + type : "";
+
+               // Handle a global trigger
+               if ( !elem ) {
+
+                       // TODO: Stop taunting the data cache; remove global events and always attach to document
+                       cache = jQuery.cache;
+                       for ( i in cache ) {
+                               if ( cache[ i ].events && cache[ i ].events[ type ] ) {
+                                       jQuery.event.trigger( event, data, cache[ i ].handle.elem, true );
+                               }
+                       }
+                       return;
+               }
+
+               // Clean up the event in case it is being reused
+               event.result = undefined;
+               if ( !event.target ) {
+                       event.target = elem;
+               }
+
+               // Clone any incoming data and prepend the event, creating the handler arg list
+               data = data != null ? jQuery.makeArray( data ) : [];
+               data.unshift( event );
+
+               // Allow special events to draw outside the lines
+               special = jQuery.event.special[ type ] || {};
+               if ( special.trigger && special.trigger.apply( elem, data ) === false ) {
+                       return;
+               }
+
+               // Determine event propagation path in advance, per W3C events spec (#9951)
+               // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+               eventPath = [[ elem, special.bindType || type ]];
+               if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+
+                       bubbleType = special.delegateType || type;
+                       cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode;
+                       for ( old = elem; cur; cur = cur.parentNode ) {
+                               eventPath.push([ cur, bubbleType ]);
+                               old = cur;
+                       }
+
+                       // Only add window if we got to document (e.g., not plain obj or detached DOM)
+                       if ( old === (elem.ownerDocument || document) ) {
+                               eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]);
+                       }
+               }
+
+               // Fire handlers on the event path
+               for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) {
+
+                       cur = eventPath[i][0];
+                       event.type = eventPath[i][1];
+
+                       handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
+                       if ( handle ) {
+                               handle.apply( cur, data );
+                       }
+                       // Note that this is a bare JS function and not a jQuery handler
+                       handle = ontype && cur[ ontype ];
+                       if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
+                               event.preventDefault();
+                       }
+               }
+               event.type = type;
+
+               // If nobody prevented the default action, do it now
+               if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+                       if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) &&
+                               !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) {
+
+                               // Call a native DOM method on the target with the same name name as the event.
+                               // Can't use an .isFunction() check here because IE6/7 fails that test.
+                               // Don't do default actions on window, that's where global variables be (#6170)
+                               // IE<9 dies on focus/blur to hidden element (#1486)
+                               if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) {
+
+                                       // Don't re-trigger an onFOO event when we call its FOO() method
+                                       old = elem[ ontype ];
+
+                                       if ( old ) {
+                                               elem[ ontype ] = null;
+                                       }
+
+                                       // Prevent re-triggering of the same event, since we already bubbled it above
+                                       jQuery.event.triggered = type;
+                                       elem[ type ]();
+                                       jQuery.event.triggered = undefined;
+
+                                       if ( old ) {
+                                               elem[ ontype ] = old;
+                                       }
+                               }
+                       }
+               }
+
+               return event.result;
+       },
+
+       dispatch: function( event ) {
+
+               // Make a writable jQuery.Event from the native event object
+               event = jQuery.event.fix( event || window.event );
+
+               var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related,
+                       handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []),
+                       delegateCount = handlers.delegateCount,
+                       args = core_slice.call( arguments ),
+                       run_all = !event.exclusive && !event.namespace,
+                       special = jQuery.event.special[ event.type ] || {},
+                       handlerQueue = [];
+
+               // Use the fix-ed jQuery.Event rather than the (read-only) native event
+               args[0] = event;
+               event.delegateTarget = this;
+
+               // Call the preDispatch hook for the mapped type, and let it bail if desired
+               if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+                       return;
+               }
+
+               // Determine handlers that should run if there are delegated events
+               // Avoid non-left-click bubbling in Firefox (#3861)
+               if ( delegateCount && !(event.button && event.type === "click") ) {
+
+                       for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {
+
+                               // Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764)
+                               if ( cur.disabled !== true || event.type !== "click" ) {
+                                       selMatch = {};
+                                       matches = [];
+                                       for ( i = 0; i < delegateCount; i++ ) {
+                                               handleObj = handlers[ i ];
+                                               sel = handleObj.selector;
+
+                                               if ( selMatch[ sel ] === undefined ) {
+                                                       selMatch[ sel ] = handleObj.needsContext ?
+                                                               jQuery( sel, this ).index( cur ) >= 0 :
+                                                               jQuery.find( sel, this, null, [ cur ] ).length;
+                                               }
+                                               if ( selMatch[ sel ] ) {
+                                                       matches.push( handleObj );
+                                               }
+                                       }
+                                       if ( matches.length ) {
+                                               handlerQueue.push({ elem: cur, matches: matches });
+                                       }
+                               }
+                       }
+               }
+
+               // Add the remaining (directly-bound) handlers
+               if ( handlers.length > delegateCount ) {
+                       handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) });
+               }
+
+               // Run delegates first; they may want to stop propagation beneath us
+               for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) {
+                       matched = handlerQueue[ i ];
+                       event.currentTarget = matched.elem;
+
+                       for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) {
+                               handleObj = matched.matches[ j ];
+
+                               // Triggered event must either 1) be non-exclusive and have no namespace, or
+                               // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
+                               if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) {
+
+                                       event.data = handleObj.data;
+                                       event.handleObj = handleObj;
+
+                                       ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
+                                                       .apply( matched.elem, args );
+
+                                       if ( ret !== undefined ) {
+                                               event.result = ret;
+                                               if ( ret === false ) {
+                                                       event.preventDefault();
+                                                       event.stopPropagation();
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               // Call the postDispatch hook for the mapped type
+               if ( special.postDispatch ) {
+                       special.postDispatch.call( this, event );
+               }
+
+               return event.result;
+       },
+
+       // Includes some event props shared by KeyEvent and MouseEvent
+       // *** attrChange attrName relatedNode srcElement  are not normalized, non-W3C, deprecated, will be removed in 1.8 ***
+       props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
+
+       fixHooks: {},
+
+       keyHooks: {
+               props: "char charCode key keyCode".split(" "),
+               filter: function( event, original ) {
+
+                       // Add which for key events
+                       if ( event.which == null ) {
+                               event.which = original.charCode != null ? original.charCode : original.keyCode;
+                       }
+
+                       return event;
+               }
+       },
+
+       mouseHooks: {
+               props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
+               filter: function( event, original ) {
+                       var eventDoc, doc, body,
+                               button = original.button,
+                               fromElement = original.fromElement;
+
+                       // Calculate pageX/Y if missing and clientX/Y available
+                       if ( event.pageX == null && original.clientX != null ) {
+                               eventDoc = event.target.ownerDocument || document;
+                               doc = eventDoc.documentElement;
+                               body = eventDoc.body;
+
+                               event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+                               event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
+                       }
+
+                       // Add relatedTarget, if necessary
+                       if ( !event.relatedTarget && fromElement ) {
+                               event.relatedTarget = fromElement === event.target ? original.toElement : fromElement;
+                       }
+
+                       // Add which for click: 1 === left; 2 === middle; 3 === right
+                       // Note: button is not normalized, so don't use it
+                       if ( !event.which && button !== undefined ) {
+                               event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+                       }
+
+                       return event;
+               }
+       },
+
+       fix: function( event ) {
+               if ( event[ jQuery.expando ] ) {
+                       return event;
+               }
+
+               // Create a writable copy of the event object and normalize some properties
+               var i, prop,
+                       originalEvent = event,
+                       fixHook = jQuery.event.fixHooks[ event.type ] || {},
+                       copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
+
+               event = jQuery.Event( originalEvent );
+
+               for ( i = copy.length; i; ) {
+                       prop = copy[ --i ];
+                       event[ prop ] = originalEvent[ prop ];
+               }
+
+               // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2)
+               if ( !event.target ) {
+                       event.target = originalEvent.srcElement || document;
+               }
+
+               // Target should not be a text node (#504, Safari)
+               if ( event.target.nodeType === 3 ) {
+                       event.target = event.target.parentNode;
+               }
+
+               // For mouse/key events, metaKey==false if it's undefined (#3368, #11328; IE6/7/8)
+               event.metaKey = !!event.metaKey;
+
+               return fixHook.filter? fixHook.filter( event, originalEvent ) : event;
+       },
+
+       special: {
+               load: {
+                       // Prevent triggered image.load events from bubbling to window.load
+                       noBubble: true
+               },
+
+               focus: {
+                       delegateType: "focusin"
+               },
+               blur: {
+                       delegateType: "focusout"
+               },
+
+               beforeunload: {
+                       setup: function( data, namespaces, eventHandle ) {
+                               // We only want to do this special case on windows
+                               if ( jQuery.isWindow( this ) ) {
+                                       this.onbeforeunload = eventHandle;
+                               }
+                       },
+
+                       teardown: function( namespaces, eventHandle ) {
+                               if ( this.onbeforeunload === eventHandle ) {
+                                       this.onbeforeunload = null;
+                               }
+                       }
+               }
+       },
+
+       simulate: function( type, elem, event, bubble ) {
+               // Piggyback on a donor event to simulate a different one.
+               // Fake originalEvent to avoid donor's stopPropagation, but if the
+               // simulated event prevents default then we do the same on the donor.
+               var e = jQuery.extend(
+                       new jQuery.Event(),
+                       event,
+                       { type: type,
+                               isSimulated: true,
+                               originalEvent: {}
+                       }
+               );
+               if ( bubble ) {
+                       jQuery.event.trigger( e, null, elem );
+               } else {
+                       jQuery.event.dispatch.call( elem, e );
+               }
+               if ( e.isDefaultPrevented() ) {
+                       event.preventDefault();
+               }
+       }
+};
+
+// Some plugins are using, but it's undocumented/deprecated and will be removed.
+// The 1.7 special event interface should provide all the hooks needed now.
+jQuery.event.handle = jQuery.event.dispatch;
+
+jQuery.removeEvent = document.removeEventListener ?
+       function( elem, type, handle ) {
+               if ( elem.removeEventListener ) {
+                       elem.removeEventListener( type, handle, false );
+               }
+       } :
+       function( elem, type, handle ) {
+               var name = "on" + type;
+
+               if ( elem.detachEvent ) {
+
+                       // #8545, #7054, preventing memory leaks for custom events in IE6-8
+                       // detachEvent needed property on element, by name of that event, to properly expose it to GC
+                       if ( typeof elem[ name ] === "undefined" ) {
+                               elem[ name ] = null;
+                       }
+
+                       elem.detachEvent( name, handle );
+               }
+       };
+
+jQuery.Event = function( src, props ) {
+       // Allow instantiation without the 'new' keyword
+       if ( !(this instanceof jQuery.Event) ) {
+               return new jQuery.Event( src, props );
+       }
+
+       // Event object
+       if ( src && src.type ) {
+               this.originalEvent = src;
+               this.type = src.type;
+
+               // Events bubbling up the document may have been marked as prevented
+               // by a handler lower down the tree; reflect the correct value.
+               this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
+                       src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
+
+       // Event type
+       } else {
+               this.type = src;
+       }
+
+       // Put explicitly provided properties onto the event object
+       if ( props ) {
+               jQuery.extend( this, props );
+       }
+
+       // Create a timestamp if incoming event doesn't have one
+       this.timeStamp = src && src.timeStamp || jQuery.now();
+
+       // Mark it as fixed
+       this[ jQuery.expando ] = true;
+};
+
+function returnFalse() {
+       return false;
+}
+function returnTrue() {
+       return true;
+}
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+       preventDefault: function() {
+               this.isDefaultPrevented = returnTrue;
+
+               var e = this.originalEvent;
+               if ( !e ) {
+                       return;
+               }
+
+               // if preventDefault exists run it on the original event
+               if ( e.preventDefault ) {
+                       e.preventDefault();
+
+               // otherwise set the returnValue property of the original event to false (IE)
+               } else {
+                       e.returnValue = false;
+               }
+       },
+       stopPropagation: function() {
+               this.isPropagationStopped = returnTrue;
+
+               var e = this.originalEvent;
+               if ( !e ) {
+                       return;
+               }
+               // if stopPropagation exists run it on the original event
+               if ( e.stopPropagation ) {
+                       e.stopPropagation();
+               }
+               // otherwise set the cancelBubble property of the original event to true (IE)
+               e.cancelBubble = true;
+       },
+       stopImmediatePropagation: function() {
+               this.isImmediatePropagationStopped = returnTrue;
+               this.stopPropagation();
+       },
+       isDefaultPrevented: returnFalse,
+       isPropagationStopped: returnFalse,
+       isImmediatePropagationStopped: returnFalse
+};
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+jQuery.each({
+       mouseenter: "mouseover",
+       mouseleave: "mouseout"
+}, function( orig, fix ) {
+       jQuery.event.special[ orig ] = {
+               delegateType: fix,
+               bindType: fix,
+
+               handle: function( event ) {
+                       var ret,
+                               target = this,
+                               related = event.relatedTarget,
+                               handleObj = event.handleObj,
+                               selector = handleObj.selector;
+
+                       // For mousenter/leave call the handler if related is outside the target.
+                       // NB: No relatedTarget if the mouse left/entered the browser window
+                       if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
+                               event.type = handleObj.origType;
+                               ret = handleObj.handler.apply( this, arguments );
+                               event.type = fix;
+                       }
+                       return ret;
+               }
+       };
+});
+
+// IE submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
+       jQuery.event.special.submit = {
+               setup: function() {
+                       // Only need this for delegated form submit events
+                       if ( jQuery.nodeName( this, "form" ) ) {
+                               return false;
+                       }
+
+                       // Lazy-add a submit handler when a descendant form may potentially be submitted
+                       jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
+                               // Node name check avoids a VML-related crash in IE (#9807)
+                               var elem = e.target,
+                                       form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
+                               if ( form && !jQuery._data( form, "_submit_attached" ) ) {
+                                       jQuery.event.add( form, "submit._submit", function( event ) {
+                                               event._submit_bubble = true;
+                                       });
+                                       jQuery._data( form, "_submit_attached", true );
+                               }
+                       });
+                       // return undefined since we don't need an event listener
+               },
+
+               postDispatch: function( event ) {
+                       // If form was submitted by the user, bubble the event up the tree
+                       if ( event._submit_bubble ) {
+                               delete event._submit_bubble;
+                               if ( this.parentNode && !event.isTrigger ) {
+                                       jQuery.event.simulate( "submit", this.parentNode, event, true );
+                               }
+                       }
+               },
+
+               teardown: function() {
+                       // Only need this for delegated form submit events
+                       if ( jQuery.nodeName( this, "form" ) ) {
+                               return false;
+                       }
+
+                       // Remove delegated handlers; cleanData eventually reaps submit handlers attached above
+                       jQuery.event.remove( this, "._submit" );
+               }
+       };
+}
+
+// IE change delegation and checkbox/radio fix
+if ( !jQuery.support.changeBubbles ) {
+
+       jQuery.event.special.change = {
+
+               setup: function() {
+
+                       if ( rformElems.test( this.nodeName ) ) {
+                               // IE doesn't fire change on a check/radio until blur; trigger it on click
+                               // after a propertychange. Eat the blur-change in special.change.handle.
+                               // This still fires onchange a second time for check/radio after blur.
+                               if ( this.type === "checkbox" || this.type === "radio" ) {
+                                       jQuery.event.add( this, "propertychange._change", function( event ) {
+                                               if ( event.originalEvent.propertyName === "checked" ) {
+                                                       this._just_changed = true;
+                                               }
+                                       });
+                                       jQuery.event.add( this, "click._change", function( event ) {
+                                               if ( this._just_changed && !event.isTrigger ) {
+                                                       this._just_changed = false;
+                                               }
+                                               // Allow triggered, simulated change events (#11500)
+                                               jQuery.event.simulate( "change", this, event, true );
+                                       });
+                               }
+                               return false;
+                       }
+                       // Delegated event; lazy-add a change handler on descendant inputs
+                       jQuery.event.add( this, "beforeactivate._change", function( e ) {
+                               var elem = e.target;
+
+                               if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "_change_attached" ) ) {
+                                       jQuery.event.add( elem, "change._change", function( event ) {
+                                               if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
+                                                       jQuery.event.simulate( "change", this.parentNode, event, true );
+                                               }
+                                       });
+                                       jQuery._data( elem, "_change_attached", true );
+                               }
+                       });
+               },
+
+               handle: function( event ) {
+                       var elem = event.target;
+
+                       // Swallow native change events from checkbox/radio, we already triggered them above
+                       if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
+                               return event.handleObj.handler.apply( this, arguments );
+                       }
+               },
+
+               teardown: function() {
+                       jQuery.event.remove( this, "._change" );
+
+                       return !rformElems.test( this.nodeName );
+               }
+       };
+}
+
+// Create "bubbling" focus and blur events
+if ( !jQuery.support.focusinBubbles ) {
+       jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+               // Attach a single capturing handler while someone wants focusin/focusout
+               var attaches = 0,
+                       handler = function( event ) {
+                               jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
+                       };
+
+               jQuery.event.special[ fix ] = {
+                       setup: function() {
+                               if ( attaches++ === 0 ) {
+                                       document.addEventListener( orig, handler, true );
+                               }
+                       },
+                       teardown: function() {
+                               if ( --attaches === 0 ) {
+                                       document.removeEventListener( orig, handler, true );
+                               }
+                       }
+               };
+       });
+}
+
+jQuery.fn.extend({
+
+       on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
+               var origFn, type;
+
+               // Types can be a map of types/handlers
+               if ( typeof types === "object" ) {
+                       // ( types-Object, selector, data )
+                       if ( typeof selector !== "string" ) { // && selector != null
+                               // ( types-Object, data )
+                               data = data || selector;
+                               selector = undefined;
+                       }
+                       for ( type in types ) {
+                               this.on( type, selector, data, types[ type ], one );
+                       }
+                       return this;
+               }
+
+               if ( data == null && fn == null ) {
+                       // ( types, fn )
+                       fn = selector;
+                       data = selector = undefined;
+               } else if ( fn == null ) {
+                       if ( typeof selector === "string" ) {
+                               // ( types, selector, fn )
+                               fn = data;
+                               data = undefined;
+                       } else {
+                               // ( types, data, fn )
+                               fn = data;
+                               data = selector;
+                               selector = undefined;
+                       }
+               }
+               if ( fn === false ) {
+                       fn = returnFalse;
+               } else if ( !fn ) {
+                       return this;
+               }
+
+               if ( one === 1 ) {
+                       origFn = fn;
+                       fn = function( event ) {
+                               // Can use an empty set, since event contains the info
+                               jQuery().off( event );
+                               return origFn.apply( this, arguments );
+                       };
+                       // Use same guid so caller can remove using origFn
+                       fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+               }
+               return this.each( function() {
+                       jQuery.event.add( this, types, fn, data, selector );
+               });
+       },
+       one: function( types, selector, data, fn ) {
+               return this.on( types, selector, data, fn, 1 );
+       },
+       off: function( types, selector, fn ) {
+               var handleObj, type;
+               if ( types && types.preventDefault && types.handleObj ) {
+                       // ( event )  dispatched jQuery.Event
+                       handleObj = types.handleObj;
+                       jQuery( types.delegateTarget ).off(
+                               handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
+                               handleObj.selector,
+                               handleObj.handler
+                       );
+                       return this;
+               }
+               if ( typeof types === "object" ) {
+                       // ( types-object [, selector] )
+                       for ( type in types ) {
+                               this.off( type, selector, types[ type ] );
+                       }
+                       return this;
+               }
+               if ( selector === false || typeof selector === "function" ) {
+                       // ( types [, fn] )
+                       fn = selector;
+                       selector = undefined;
+               }
+               if ( fn === false ) {
+                       fn = returnFalse;
+               }
+               return this.each(function() {
+                       jQuery.event.remove( this, types, fn, selector );
+               });
+       },
+
+       bind: function( types, data, fn ) {
+               return this.on( types, null, data, fn );
+       },
+       unbind: function( types, fn ) {
+               return this.off( types, null, fn );
+       },
+
+       live: function( types, data, fn ) {
+               jQuery( this.context ).on( types, this.selector, data, fn );
+               return this;
+       },
+       die: function( types, fn ) {
+               jQuery( this.context ).off( types, this.selector || "**", fn );
+               return this;
+       },
+
+       delegate: function( selector, types, data, fn ) {
+               return this.on( types, selector, data, fn );
+       },
+       undelegate: function( selector, types, fn ) {
+               // ( namespace ) or ( selector, types [, fn] )
+               return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
+       },
+
+       trigger: function( type, data ) {
+               return this.each(function() {
+                       jQuery.event.trigger( type, data, this );
+               });
+       },
+       triggerHandler: function( type, data ) {
+               if ( this[0] ) {
+                       return jQuery.event.trigger( type, data, this[0], true );
+               }
+       },
+
+       toggle: function( fn ) {
+               // Save reference to arguments for access in closure
+               var args = arguments,
+                       guid = fn.guid || jQuery.guid++,
+                       i = 0,
+                       toggler = function( event ) {
+                               // Figure out which function to execute
+                               var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+                               jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+
+                               // Make sure that clicks stop
+                               event.preventDefault();
+
+                               // and execute the function
+                               return args[ lastToggle ].apply( this, arguments ) || false;
+                       };
+
+               // link all the functions, so any of them can unbind this click handler
+               toggler.guid = guid;
+               while ( i < args.length ) {
+                       args[ i++ ].guid = guid;
+               }
+
+               return this.click( toggler );
+       },
+
+       hover: function( fnOver, fnOut ) {
+               return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+       }
+});
+
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+       "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+       "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
+
+       // Handle event binding
+       jQuery.fn[ name ] = function( data, fn ) {
+               if ( fn == null ) {
+                       fn = data;
+                       data = null;
+               }
+
+               return arguments.length > 0 ?
+                       this.on( name, null, data, fn ) :
+                       this.trigger( name );
+       };
+
+       if ( rkeyEvent.test( name ) ) {
+               jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks;
+       }
+
+       if ( rmouseEvent.test( name ) ) {
+               jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks;
+       }
+});
+/*!\r
+ * Sizzle CSS Selector Engine\r
+ * Copyright 2012 jQuery Foundation and other contributors\r
+ * Released under the MIT license\r
+ * http://sizzlejs.com/\r
+ */\r
+(function( window, undefined ) {\r
+\r
+var cachedruns,\r
+       assertGetIdNotName,\r
+       Expr,\r
+       getText,\r
+       isXML,\r
+       contains,\r
+       compile,\r
+       sortOrder,\r
+       hasDuplicate,\r
+       outermostContext,\r
+\r
+       baseHasDuplicate = true,\r
+       strundefined = "undefined",\r
+\r
+       expando = ( "sizcache" + Math.random() ).replace( ".", "" ),\r
+\r
+       Token = String,\r
+       document = window.document,\r
+       docElem = document.documentElement,\r
+       dirruns = 0,\r
+       done = 0,\r
+       pop = [].pop,\r
+       push = [].push,\r
+       slice = [].slice,\r
+       // Use a stripped-down indexOf if a native one is unavailable\r
+       indexOf = [].indexOf || function( elem ) {\r
+               var i = 0,\r
+                       len = this.length;\r
+               for ( ; i < len; i++ ) {\r
+                       if ( this[i] === elem ) {\r
+                               return i;\r
+                       }\r
+               }\r
+               return -1;\r
+       },\r
+\r
+       // Augment a function for special use by Sizzle\r
+       markFunction = function( fn, value ) {\r
+               fn[ expando ] = value == null || value;\r
+               return fn;\r
+       },\r
+\r
+       createCache = function() {\r
+               var cache = {},\r
+                       keys = [];\r
+\r
+               return markFunction(function( key, value ) {\r
+                       // Only keep the most recent entries\r
+                       if ( keys.push( key ) > Expr.cacheLength ) {\r
+                               delete cache[ keys.shift() ];\r
+                       }\r
+\r
+                       // Retrieve with (key + " ") to avoid collision with native Object.prototype properties (see Issue #157)\r
+                       return (cache[ key + " " ] = value);\r
+               }, cache );\r
+       },\r
+\r
+       classCache = createCache(),\r
+       tokenCache = createCache(),\r
+       compilerCache = createCache(),\r
+\r
+       // Regex\r
+\r
+       // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace\r
+       whitespace = "[\\x20\\t\\r\\n\\f]",\r
+       // http://www.w3.org/TR/css3-syntax/#characters\r
+       characterEncoding = "(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",\r
+\r
+       // Loosely modeled on CSS identifier characters\r
+       // An unquoted value should be a CSS identifier (http://www.w3.org/TR/css3-selectors/#attribute-selectors)\r
+       // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier\r
+       identifier = characterEncoding.replace( "w", "w#" ),\r
+\r
+       // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors\r
+       operators = "([*^$|!~]?=)",\r
+       attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace +\r
+               "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]",\r
+\r
+       // Prefer arguments not in parens/brackets,\r
+       //   then attribute selectors and non-pseudos (denoted by :),\r
+       //   then anything else\r
+       // These preferences are here to reduce the number of selectors\r
+       //   needing tokenize in the PSEUDO preFilter\r
+       pseudos = ":(" + characterEncoding + ")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:" + attributes + ")|[^:]|\\\\.)*|.*))\\)|)",\r
+\r
+       // For matchExpr.POS and matchExpr.needsContext\r
+       pos = ":(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace +\r
+               "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)",\r
+\r
+       // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\r
+       rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),\r
+\r
+       rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),\r
+       rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ),\r
+       rpseudo = new RegExp( pseudos ),\r
+\r
+       // Easily-parseable/retrievable ID or TAG or CLASS selectors\r
+       rquickExpr = /^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,\r
+\r
+       rnot = /^:not/,\r
+       rsibling = /[\x20\t\r\n\f]*[+~]/,\r
+       rendsWithNot = /:not\($/,\r
+\r
+       rheader = /h\d/i,\r
+       rinputs = /input|select|textarea|button/i,\r
+\r
+       rbackslash = /\\(?!\\)/g,\r
+\r
+       matchExpr = {\r
+               "ID": new RegExp( "^#(" + characterEncoding + ")" ),\r
+               "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),\r
+               "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ),\r
+               "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),\r
+               "ATTR": new RegExp( "^" + attributes ),\r
+               "PSEUDO": new RegExp( "^" + pseudos ),\r
+               "POS": new RegExp( pos, "i" ),\r
+               "CHILD": new RegExp( "^:(only|nth|first|last)-child(?:\\(" + whitespace +\r
+                       "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +\r
+                       "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),\r
+               // For use in libraries implementing .is()\r
+               "needsContext": new RegExp( "^" + whitespace + "*[>+~]|" + pos, "i" )\r
+       },\r
+\r
+       // Support\r
+\r
+       // Used for testing something on an element\r
+       assert = function( fn ) {\r
+               var div = document.createElement("div");\r
+\r
+               try {\r
+                       return fn( div );\r
+               } catch (e) {\r
+                       return false;\r
+               } finally {\r
+                       // release memory in IE\r
+                       div = null;\r
+               }\r
+       },\r
+\r
+       // Check if getElementsByTagName("*") returns only elements\r
+       assertTagNameNoComments = assert(function( div ) {\r
+               div.appendChild( document.createComment("") );\r
+               return !div.getElementsByTagName("*").length;\r
+       }),\r
+\r
+       // Check if getAttribute returns normalized href attributes\r
+       assertHrefNotNormalized = assert(function( div ) {\r
+               div.innerHTML = "<a href='#'></a>";\r
+               return div.firstChild && typeof div.firstChild.getAttribute !== strundefined &&\r
+                       div.firstChild.getAttribute("href") === "#";\r
+       }),\r
+\r
+       // Check if attributes should be retrieved by attribute nodes\r
+       assertAttributes = assert(function( div ) {\r
+               div.innerHTML = "<select></select>";\r
+               var type = typeof div.lastChild.getAttribute("multiple");\r
+               // IE8 returns a string for some attributes even when not present\r
+               return type !== "boolean" && type !== "string";\r
+       }),\r
+\r
+       // Check if getElementsByClassName can be trusted\r
+       assertUsableClassName = assert(function( div ) {\r
+               // Opera can't find a second classname (in 9.6)\r
+               div.innerHTML = "<div class='hidden e'></div><div class='hidden'></div>";\r
+               if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) {\r
+                       return false;\r
+               }\r
+\r
+               // Safari 3.2 caches class attributes and doesn't catch changes\r
+               div.lastChild.className = "e";\r
+               return div.getElementsByClassName("e").length === 2;\r
+       }),\r
+\r
+       // Check if getElementById returns elements by name\r
+       // Check if getElementsByName privileges form controls or returns elements by ID\r
+       assertUsableName = assert(function( div ) {\r
+               // Inject content\r
+               div.id = expando + 0;\r
+               div.innerHTML = "<a name='" + expando + "'></a><div name='" + expando + "'></div>";\r
+               docElem.insertBefore( div, docElem.firstChild );\r
+\r
+               // Test\r
+               var pass = document.getElementsByName &&\r
+                       // buggy browsers will return fewer than the correct 2\r
+                       document.getElementsByName( expando ).length === 2 +\r
+                       // buggy browsers will return more than the correct 0\r
+                       document.getElementsByName( expando + 0 ).length;\r
+               assertGetIdNotName = !document.getElementById( expando );\r
+\r
+               // Cleanup\r
+               docElem.removeChild( div );\r
+\r
+               return pass;\r
+       });\r
+\r
+// If slice is not available, provide a backup\r
+try {\r
+       slice.call( docElem.childNodes, 0 )[0].nodeType;\r
+} catch ( e ) {\r
+       slice = function( i ) {\r
+               var elem,\r
+                       results = [];\r
+               for ( ; (elem = this[i]); i++ ) {\r
+                       results.push( elem );\r
+               }\r
+               return results;\r
+       };\r
+}\r
+\r
+function Sizzle( selector, context, results, seed ) {\r
+       results = results || [];\r
+       context = context || document;\r
+       var match, elem, xml, m,\r
+               nodeType = context.nodeType;\r
+\r
+       if ( !selector || typeof selector !== "string" ) {\r
+               return results;\r
+       }\r
+\r
+       if ( nodeType !== 1 && nodeType !== 9 ) {\r
+               return [];\r
+       }\r
+\r
+       xml = isXML( context );\r
+\r
+       if ( !xml && !seed ) {\r
+               if ( (match = rquickExpr.exec( selector )) ) {\r
+                       // Speed-up: Sizzle("#ID")\r
+                       if ( (m = match[1]) ) {\r
+                               if ( nodeType === 9 ) {\r
+                                       elem = context.getElementById( m );\r
+                                       // Check parentNode to catch when Blackberry 4.6 returns\r
+                                       // nodes that are no longer in the document #6963\r
+                                       if ( elem && elem.parentNode ) {\r
+                                               // Handle the case where IE, Opera, and Webkit return items\r
+                                               // by name instead of ID\r
+                                               if ( elem.id === m ) {\r
+                                                       results.push( elem );\r
+                                                       return results;\r
+                                               }\r
+                                       } else {\r
+                                               return results;\r
+                                       }\r
+                               } else {\r
+                                       // Context is not a document\r
+                                       if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&\r
+                                               contains( context, elem ) && elem.id === m ) {\r
+                                               results.push( elem );\r
+                                               return results;\r
+                                       }\r
+                               }\r
+\r
+                       // Speed-up: Sizzle("TAG")\r
+                       } else if ( match[2] ) {\r
+                               push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) );\r
+                               return results;\r
+\r
+                       // Speed-up: Sizzle(".CLASS")\r
+                       } else if ( (m = match[3]) && assertUsableClassName && context.getElementsByClassName ) {\r
+                               push.apply( results, slice.call(context.getElementsByClassName( m ), 0) );\r
+                               return results;\r
+                       }\r
+               }\r
+       }\r
+\r
+       // All others\r
+       return select( selector.replace( rtrim, "$1" ), context, results, seed, xml );\r
+}\r
+\r
+Sizzle.matches = function( expr, elements ) {\r
+       return Sizzle( expr, null, null, elements );\r
+};\r
+\r
+Sizzle.matchesSelector = function( elem, expr ) {\r
+       return Sizzle( expr, null, null, [ elem ] ).length > 0;\r
+};\r
+\r
+// Returns a function to use in pseudos for input types\r
+function createInputPseudo( type ) {\r
+       return function( elem ) {\r
+               var name = elem.nodeName.toLowerCase();\r
+               return name === "input" && elem.type === type;\r
+       };\r
+}\r
+\r
+// Returns a function to use in pseudos for buttons\r
+function createButtonPseudo( type ) {\r
+       return function( elem ) {\r
+               var name = elem.nodeName.toLowerCase();\r
+               return (name === "input" || name === "button") && elem.type === type;\r
+       };\r
+}\r
+\r
+// Returns a function to use in pseudos for positionals\r
+function createPositionalPseudo( fn ) {\r
+       return markFunction(function( argument ) {\r
+               argument = +argument;\r
+               return markFunction(function( seed, matches ) {\r
+                       var j,\r
+                               matchIndexes = fn( [], seed.length, argument ),\r
+                               i = matchIndexes.length;\r
+\r
+                       // Match elements found at the specified indexes\r
+                       while ( i-- ) {\r
+                               if ( seed[ (j = matchIndexes[i]) ] ) {\r
+                                       seed[j] = !(matches[j] = seed[j]);\r
+                               }\r
+                       }\r
+               });\r
+       });\r
+}\r
+\r
+/**\r
+ * Utility function for retrieving the text value of an array of DOM nodes\r
+ * @param {Array|Element} elem\r
+ */\r
+getText = Sizzle.getText = function( elem ) {\r
+       var node,\r
+               ret = "",\r
+               i = 0,\r
+               nodeType = elem.nodeType;\r
+\r
+       if ( nodeType ) {\r
+               if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {\r
+                       // Use textContent for elements\r
+                       // innerText usage removed for consistency of new lines (see #11153)\r
+                       if ( typeof elem.textContent === "string" ) {\r
+                               return elem.textContent;\r
+                       } else {\r
+                               // Traverse its children\r
+                               for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\r
+                                       ret += getText( elem );\r
+                               }\r
+                       }\r
+               } else if ( nodeType === 3 || nodeType === 4 ) {\r
+                       return elem.nodeValue;\r
+               }\r
+               // Do not include comment or processing instruction nodes\r
+       } else {\r
+\r
+               // If no nodeType, this is expected to be an array\r
+               for ( ; (node = elem[i]); i++ ) {\r
+                       // Do not traverse comment nodes\r
+                       ret += getText( node );\r
+               }\r
+       }\r
+       return ret;\r
+};\r
+\r
+isXML = Sizzle.isXML = function( elem ) {\r
+       // documentElement is verified for cases where it doesn't yet exist\r
+       // (such as loading iframes in IE - #4833)\r
+       var documentElement = elem && (elem.ownerDocument || elem).documentElement;\r
+       return documentElement ? documentElement.nodeName !== "HTML" : false;\r
+};\r
+\r
+// Element contains another\r
+contains = Sizzle.contains = docElem.contains ?\r
+       function( a, b ) {\r
+               var adown = a.nodeType === 9 ? a.documentElement : a,\r
+                       bup = b && b.parentNode;\r
+               return a === bup || !!( bup && bup.nodeType === 1 && adown.contains && adown.contains(bup) );\r
+       } :\r
+       docElem.compareDocumentPosition ?\r
+       function( a, b ) {\r
+               return b && !!( a.compareDocumentPosition( b ) & 16 );\r
+       } :\r
+       function( a, b ) {\r
+               while ( (b = b.parentNode) ) {\r
+                       if ( b === a ) {\r
+                               return true;\r
+                       }\r
+               }\r
+               return false;\r
+       };\r
+\r
+Sizzle.attr = function( elem, name ) {\r
+       var val,\r
+               xml = isXML( elem );\r
+\r
+       if ( !xml ) {\r
+               name = name.toLowerCase();\r
+       }\r
+       if ( (val = Expr.attrHandle[ name ]) ) {\r
+               return val( elem );\r
+       }\r
+       if ( xml || assertAttributes ) {\r
+               return elem.getAttribute( name );\r
+       }\r
+       val = elem.getAttributeNode( name );\r
+       return val ?\r
+               typeof elem[ name ] === "boolean" ?\r
+                       elem[ name ] ? name : null :\r
+                       val.specified ? val.value : null :\r
+               null;\r
+};\r
+\r
+Expr = Sizzle.selectors = {\r
+\r
+       // Can be adjusted by the user\r
+       cacheLength: 50,\r
+\r
+       createPseudo: markFunction,\r
+\r
+       match: matchExpr,\r
+\r
+       // IE6/7 return a modified href\r
+       attrHandle: assertHrefNotNormalized ?\r
+               {} :\r
+               {\r
+                       "href": function( elem ) {\r
+                               return elem.getAttribute( "href", 2 );\r
+                       },\r
+                       "type": function( elem ) {\r
+                               return elem.getAttribute("type");\r
+                       }\r
+               },\r
+\r
+       find: {\r
+               "ID": assertGetIdNotName ?\r
+                       function( id, context, xml ) {\r
+                               if ( typeof context.getElementById !== strundefined && !xml ) {\r
+                                       var m = context.getElementById( id );\r
+                                       // Check parentNode to catch when Blackberry 4.6 returns\r
+                                       // nodes that are no longer in the document #6963\r
+                                       return m && m.parentNode ? [m] : [];\r
+                               }\r
+                       } :\r
+                       function( id, context, xml ) {\r
+                               if ( typeof context.getElementById !== strundefined && !xml ) {\r
+                                       var m = context.getElementById( id );\r
+\r
+                                       return m ?\r
+                                               m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ?\r
+                                                       [m] :\r
+                                                       undefined :\r
+                                               [];\r
+                               }\r
+                       },\r
+\r
+               "TAG": assertTagNameNoComments ?\r
+                       function( tag, context ) {\r
+                               if ( typeof context.getElementsByTagName !== strundefined ) {\r
+                                       return context.getElementsByTagName( tag );\r
+                               }\r
+                       } :\r
+                       function( tag, context ) {\r
+                               var results = context.getElementsByTagName( tag );\r
+\r
+                               // Filter out possible comments\r
+                               if ( tag === "*" ) {\r
+                                       var elem,\r
+                                               tmp = [],\r
+                                               i = 0;\r
+\r
+                                       for ( ; (elem = results[i]); i++ ) {\r
+                                               if ( elem.nodeType === 1 ) {\r
+                                                       tmp.push( elem );\r
+                                               }\r
+                                       }\r
+\r
+                                       return tmp;\r
+                               }\r
+                               return results;\r
+                       },\r
+\r
+               "NAME": assertUsableName && function( tag, context ) {\r
+                       if ( typeof context.getElementsByName !== strundefined ) {\r
+                               return context.getElementsByName( name );\r
+                       }\r
+               },\r
+\r
+               "CLASS": assertUsableClassName && function( className, context, xml ) {\r
+                       if ( typeof context.getElementsByClassName !== strundefined && !xml ) {\r
+                               return context.getElementsByClassName( className );\r
+                       }\r
+               }\r
+       },\r
+\r
+       relative: {\r
+               ">": { dir: "parentNode", first: true },\r
+               " ": { dir: "parentNode" },\r
+               "+": { dir: "previousSibling", first: true },\r
+               "~": { dir: "previousSibling" }\r
+       },\r
+\r
+       preFilter: {\r
+               "ATTR": function( match ) {\r
+                       match[1] = match[1].replace( rbackslash, "" );\r
+\r
+                       // Move the given value to match[3] whether quoted or unquoted\r
+                       match[3] = ( match[4] || match[5] || "" ).replace( rbackslash, "" );\r
+\r
+                       if ( match[2] === "~=" ) {\r
+                               match[3] = " " + match[3] + " ";\r
+                       }\r
+\r
+                       return match.slice( 0, 4 );\r
+               },\r
+\r
+               "CHILD": function( match ) {\r
+                       /* matches from matchExpr["CHILD"]\r
+                               1 type (only|nth|...)\r
+                               2 argument (even|odd|\d*|\d*n([+-]\d+)?|...)\r
+                               3 xn-component of xn+y argument ([+-]?\d*n|)\r
+                               4 sign of xn-component\r
+                               5 x of xn-component\r
+                               6 sign of y-component\r
+                               7 y of y-component\r
+                       */\r
+                       match[1] = match[1].toLowerCase();\r
+\r
+                       if ( match[1] === "nth" ) {\r
+                               // nth-child requires argument\r
+                               if ( !match[2] ) {\r
+                                       Sizzle.error( match[0] );\r
+                               }\r
+\r
+                               // numeric x and y parameters for Expr.filter.CHILD\r
+                               // remember that false/true cast respectively to 0/1\r
+                               match[3] = +( match[3] ? match[4] + (match[5] || 1) : 2 * ( match[2] === "even" || match[2] === "odd" ) );\r
+                               match[4] = +( ( match[6] + match[7] ) || match[2] === "odd" );\r
+\r
+                       // other types prohibit arguments\r
+                       } else if ( match[2] ) {\r
+                               Sizzle.error( match[0] );\r
+                       }\r
+\r
+                       return match;\r
+               },\r
+\r
+               "PSEUDO": function( match ) {\r
+                       var unquoted, excess;\r
+                       if ( matchExpr["CHILD"].test( match[0] ) ) {\r
+                               return null;\r
+                       }\r
+\r
+                       if ( match[3] ) {\r
+                               match[2] = match[3];\r
+                       } else if ( (unquoted = match[4]) ) {\r
+                               // Only check arguments that contain a pseudo\r
+                               if ( rpseudo.test(unquoted) &&\r
+                                       // Get excess from tokenize (recursively)\r
+                                       (excess = tokenize( unquoted, true )) &&\r
+                                       // advance to the next closing parenthesis\r
+                                       (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {\r
+\r
+                                       // excess is a negative index\r
+                                       unquoted = unquoted.slice( 0, excess );\r
+                                       match[0] = match[0].slice( 0, excess );\r
+                               }\r
+                               match[2] = unquoted;\r
+                       }\r
+\r
+                       // Return only captures needed by the pseudo filter method (type and argument)\r
+                       return match.slice( 0, 3 );\r
+               }\r
+       },\r
+\r
+       filter: {\r
+               "ID": assertGetIdNotName ?\r
+                       function( id ) {\r
+                               id = id.replace( rbackslash, "" );\r
+                               return function( elem ) {\r
+                                       return elem.getAttribute("id") === id;\r
+                               };\r
+                       } :\r
+                       function( id ) {\r
+                               id = id.replace( rbackslash, "" );\r
+                               return function( elem ) {\r
+                                       var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");\r
+                                       return node && node.value === id;\r
+                               };\r
+                       },\r
+\r
+               "TAG": function( nodeName ) {\r
+                       if ( nodeName === "*" ) {\r
+                               return function() { return true; };\r
+                       }\r
+                       nodeName = nodeName.replace( rbackslash, "" ).toLowerCase();\r
+\r
+                       return function( elem ) {\r
+                               return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;\r
+                       };\r
+               },\r
+\r
+               "CLASS": function( className ) {\r
+                       var pattern = classCache[ expando ][ className + " " ];\r
+\r
+                       return pattern ||\r
+                               (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&\r
+                               classCache( className, function( elem ) {\r
+                                       return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" );\r
+                               });\r
+               },\r
+\r
+               "ATTR": function( name, operator, check ) {\r
+                       return function( elem, context ) {\r
+                               var result = Sizzle.attr( elem, name );\r
+\r
+                               if ( result == null ) {\r
+                                       return operator === "!=";\r
+                               }\r
+                               if ( !operator ) {\r
+                                       return true;\r
+                               }\r
+\r
+                               result += "";\r
+\r
+                               return operator === "=" ? result === check :\r
+                                       operator === "!=" ? result !== check :\r
+                                       operator === "^=" ? check && result.indexOf( check ) === 0 :\r
+                                       operator === "*=" ? check && result.indexOf( check ) > -1 :\r
+                                       operator === "$=" ? check && result.substr( result.length - check.length ) === check :\r
+                                       operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :\r
+                                       operator === "|=" ? result === check || result.substr( 0, check.length + 1 ) === check + "-" :\r
+                                       false;\r
+                       };\r
+               },\r
+\r
+               "CHILD": function( type, argument, first, last ) {\r
+\r
+                       if ( type === "nth" ) {\r
+                               return function( elem ) {\r
+                                       var node, diff,\r
+                                               parent = elem.parentNode;\r
+\r
+                                       if ( first === 1 && last === 0 ) {\r
+                                               return true;\r
+                                       }\r
+\r
+                                       if ( parent ) {\r
+                                               diff = 0;\r
+                                               for ( node = parent.firstChild; node; node = node.nextSibling ) {\r
+                                                       if ( node.nodeType === 1 ) {\r
+                                                               diff++;\r
+                                                               if ( elem === node ) {\r
+                                                                       break;\r
+                                                               }\r
+                                                       }\r
+                                               }\r
+                                       }\r
+\r
+                                       // Incorporate the offset (or cast to NaN), then check against cycle size\r
+                                       diff -= last;\r
+                                       return diff === first || ( diff % first === 0 && diff / first >= 0 );\r
+                               };\r
+                       }\r
+\r
+                       return function( elem ) {\r
+                               var node = elem;\r
+\r
+                               switch ( type ) {\r
+                                       case "only":\r
+                                       case "first":\r
+                                               while ( (node = node.previousSibling) ) {\r
+                                                       if ( node.nodeType === 1 ) {\r
+                                                               return false;\r
+                                                       }\r
+                                               }\r
+\r
+                                               if ( type === "first" ) {\r
+                                                       return true;\r
+                                               }\r
+\r
+                                               node = elem;\r
+\r
+                                               /* falls through */\r
+                                       case "last":\r
+                                               while ( (node = node.nextSibling) ) {\r
+                                                       if ( node.nodeType === 1 ) {\r
+                                                               return false;\r
+                                                       }\r
+                                               }\r
+\r
+                                               return true;\r
+                               }\r
+                       };\r
+               },\r
+\r
+               "PSEUDO": function( pseudo, argument ) {\r
+                       // pseudo-class names are case-insensitive\r
+                       // http://www.w3.org/TR/selectors/#pseudo-classes\r
+                       // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\r
+                       // Remember that setFilters inherits from pseudos\r
+                       var args,\r
+                               fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||\r
+                                       Sizzle.error( "unsupported pseudo: " + pseudo );\r
+\r
+                       // The user may use createPseudo to indicate that\r
+                       // arguments are needed to create the filter function\r
+                       // just as Sizzle does\r
+                       if ( fn[ expando ] ) {\r
+                               return fn( argument );\r
+                       }\r
+\r
+                       // But maintain support for old signatures\r
+                       if ( fn.length > 1 ) {\r
+                               args = [ pseudo, pseudo, "", argument ];\r
+                               return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?\r
+                                       markFunction(function( seed, matches ) {\r
+                                               var idx,\r
+                                                       matched = fn( seed, argument ),\r
+                                                       i = matched.length;\r
+                                               while ( i-- ) {\r
+                                                       idx = indexOf.call( seed, matched[i] );\r
+                                                       seed[ idx ] = !( matches[ idx ] = matched[i] );\r
+                                               }\r
+                                       }) :\r
+                                       function( elem ) {\r
+                                               return fn( elem, 0, args );\r
+                                       };\r
+                       }\r
+\r
+                       return fn;\r
+               }\r
+       },\r
+\r
+       pseudos: {\r
+               "not": markFunction(function( selector ) {\r
+                       // Trim the selector passed to compile\r
+                       // to avoid treating leading and trailing\r
+                       // spaces as combinators\r
+                       var input = [],\r
+                               results = [],\r
+                               matcher = compile( selector.replace( rtrim, "$1" ) );\r
+\r
+                       return matcher[ expando ] ?\r
+                               markFunction(function( seed, matches, context, xml ) {\r
+                                       var elem,\r
+                                               unmatched = matcher( seed, null, xml, [] ),\r
+                                               i = seed.length;\r
+\r
+                                       // Match elements unmatched by `matcher`\r
+                                       while ( i-- ) {\r
+                                               if ( (elem = unmatched[i]) ) {\r
+                                                       seed[i] = !(matches[i] = elem);\r
+                                               }\r
+                                       }\r
+                               }) :\r
+                               function( elem, context, xml ) {\r
+                                       input[0] = elem;\r
+                                       matcher( input, null, xml, results );\r
+                                       return !results.pop();\r
+                               };\r
+               }),\r
+\r
+               "has": markFunction(function( selector ) {\r
+                       return function( elem ) {\r
+                               return Sizzle( selector, elem ).length > 0;\r
+                       };\r
+               }),\r
+\r
+               "contains": markFunction(function( text ) {\r
+                       return function( elem ) {\r
+                               return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;\r
+                       };\r
+               }),\r
+\r
+               "enabled": function( elem ) {\r
+                       return elem.disabled === false;\r
+               },\r
+\r
+               "disabled": function( elem ) {\r
+                       return elem.disabled === true;\r
+               },\r
+\r
+               "checked": function( elem ) {\r
+                       // In CSS3, :checked should return both checked and selected elements\r
+                       // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\r
+                       var nodeName = elem.nodeName.toLowerCase();\r
+                       return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);\r
+               },\r
+\r
+               "selected": function( elem ) {\r
+                       // Accessing this property makes selected-by-default\r
+                       // options in Safari work properly\r
+                       if ( elem.parentNode ) {\r
+                               elem.parentNode.selectedIndex;\r
+                       }\r
+\r
+                       return elem.selected === true;\r
+               },\r
+\r
+               "parent": function( elem ) {\r
+                       return !Expr.pseudos["empty"]( elem );\r
+               },\r
+\r
+               "empty": function( elem ) {\r
+                       // http://www.w3.org/TR/selectors/#empty-pseudo\r
+                       // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),\r
+                       //   not comment, processing instructions, or others\r
+                       // Thanks to Diego Perini for the nodeName shortcut\r
+                       //   Greater than "@" means alpha characters (specifically not starting with "#" or "?")\r
+                       var nodeType;\r
+                       elem = elem.firstChild;\r
+                       while ( elem ) {\r
+                               if ( elem.nodeName > "@" || (nodeType = elem.nodeType) === 3 || nodeType === 4 ) {\r
+                                       return false;\r
+                               }\r
+                               elem = elem.nextSibling;\r
+                       }\r
+                       return true;\r
+               },\r
+\r
+               "header": function( elem ) {\r
+                       return rheader.test( elem.nodeName );\r
+               },\r
+\r
+               "text": function( elem ) {\r
+                       var type, attr;\r
+                       // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)\r
+                       // use getAttribute instead to test this case\r
+                       return elem.nodeName.toLowerCase() === "input" &&\r
+                               (type = elem.type) === "text" &&\r
+                               ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === type );\r
+               },\r
+\r
+               // Input types\r
+               "radio": createInputPseudo("radio"),\r
+               "checkbox": createInputPseudo("checkbox"),\r
+               "file": createInputPseudo("file"),\r
+               "password": createInputPseudo("password"),\r
+               "image": createInputPseudo("image"),\r
+\r
+               "submit": createButtonPseudo("submit"),\r
+               "reset": createButtonPseudo("reset"),\r
+\r
+               "button": function( elem ) {\r
+                       var name = elem.nodeName.toLowerCase();\r
+                       return name === "input" && elem.type === "button" || name === "button";\r
+               },\r
+\r
+               "input": function( elem ) {\r
+                       return rinputs.test( elem.nodeName );\r
+               },\r
+\r
+               "focus": function( elem ) {\r
+                       var doc = elem.ownerDocument;\r
+                       return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);\r
+               },\r
+\r
+               "active": function( elem ) {\r
+                       return elem === elem.ownerDocument.activeElement;\r
+               },\r
+\r
+               // Positional types\r
+               "first": createPositionalPseudo(function() {\r
+                       return [ 0 ];\r
+               }),\r
+\r
+               "last": createPositionalPseudo(function( matchIndexes, length ) {\r
+                       return [ length - 1 ];\r
+               }),\r
+\r
+               "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {\r
+                       return [ argument < 0 ? argument + length : argument ];\r
+               }),\r
+\r
+               "even": createPositionalPseudo(function( matchIndexes, length ) {\r
+                       for ( var i = 0; i < length; i += 2 ) {\r
+                               matchIndexes.push( i );\r
+                       }\r
+                       return matchIndexes;\r
+               }),\r
+\r
+               "odd": createPositionalPseudo(function( matchIndexes, length ) {\r
+                       for ( var i = 1; i < length; i += 2 ) {\r
+                               matchIndexes.push( i );\r
+                       }\r
+                       return matchIndexes;\r
+               }),\r
+\r
+               "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {\r
+                       for ( var i = argument < 0 ? argument + length : argument; --i >= 0; ) {\r
+                               matchIndexes.push( i );\r
+                       }\r
+                       return matchIndexes;\r
+               }),\r
+\r
+               "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {\r
+                       for ( var i = argument < 0 ? argument + length : argument; ++i < length; ) {\r
+                               matchIndexes.push( i );\r
+                       }\r
+                       return matchIndexes;\r
+               })\r
+       }\r
+};\r
+\r
+function siblingCheck( a, b, ret ) {\r
+       if ( a === b ) {\r
+               return ret;\r
+       }\r
+\r
+       var cur = a.nextSibling;\r
+\r
+       while ( cur ) {\r
+               if ( cur === b ) {\r
+                       return -1;\r
+               }\r
+\r
+               cur = cur.nextSibling;\r
+       }\r
+\r
+       return 1;\r
+}\r
+\r
+sortOrder = docElem.compareDocumentPosition ?\r
+       function( a, b ) {\r
+               if ( a === b ) {\r
+                       hasDuplicate = true;\r
+                       return 0;\r
+               }\r
+\r
+               return ( !a.compareDocumentPosition || !b.compareDocumentPosition ?\r
+                       a.compareDocumentPosition :\r
+                       a.compareDocumentPosition(b) & 4\r
+               ) ? -1 : 1;\r
+       } :\r
+       function( a, b ) {\r
+               // The nodes are identical, we can exit early\r
+               if ( a === b ) {\r
+                       hasDuplicate = true;\r
+                       return 0;\r
+\r
+               // Fallback to using sourceIndex (in IE) if it's available on both nodes\r
+               } else if ( a.sourceIndex && b.sourceIndex ) {\r
+                       return a.sourceIndex - b.sourceIndex;\r
+               }\r
+\r
+               var al, bl,\r
+                       ap = [],\r
+                       bp = [],\r
+                       aup = a.parentNode,\r
+                       bup = b.parentNode,\r
+                       cur = aup;\r
+\r
+               // If the nodes are siblings (or identical) we can do a quick check\r
+               if ( aup === bup ) {\r
+                       return siblingCheck( a, b );\r
+\r
+               // If no parents were found then the nodes are disconnected\r
+               } else if ( !aup ) {\r
+                       return -1;\r
+\r
+               } else if ( !bup ) {\r
+                       return 1;\r
+               }\r
+\r
+               // Otherwise they're somewhere else in the tree so we need\r
+               // to build up a full list of the parentNodes for comparison\r
+               while ( cur ) {\r
+                       ap.unshift( cur );\r
+                       cur = cur.parentNode;\r
+               }\r
+\r
+               cur = bup;\r
+\r
+               while ( cur ) {\r
+                       bp.unshift( cur );\r
+                       cur = cur.parentNode;\r
+               }\r
+\r
+               al = ap.length;\r
+               bl = bp.length;\r
+\r
+               // Start walking down the tree looking for a discrepancy\r
+               for ( var i = 0; i < al && i < bl; i++ ) {\r
+                       if ( ap[i] !== bp[i] ) {\r
+                               return siblingCheck( ap[i], bp[i] );\r
+                       }\r
+               }\r
+\r
+               // We ended someplace up the tree so do a sibling check\r
+               return i === al ?\r
+                       siblingCheck( a, bp[i], -1 ) :\r
+                       siblingCheck( ap[i], b, 1 );\r
+       };\r
+\r
+// Always assume the presence of duplicates if sort doesn't\r
+// pass them to our comparison function (as in Google Chrome).\r
+[0, 0].sort( sortOrder );\r
+baseHasDuplicate = !hasDuplicate;\r
+\r
+// Document sorting and removing duplicates\r
+Sizzle.uniqueSort = function( results ) {\r
+       var elem,\r
+               duplicates = [],\r
+               i = 1,\r
+               j = 0;\r
+\r
+       hasDuplicate = baseHasDuplicate;\r
+       results.sort( sortOrder );\r
+\r
+       if ( hasDuplicate ) {\r
+               for ( ; (elem = results[i]); i++ ) {\r
+                       if ( elem === results[ i - 1 ] ) {\r
+                               j = duplicates.push( i );\r
+                       }\r
+               }\r
+               while ( j-- ) {\r
+                       results.splice( duplicates[ j ], 1 );\r
+               }\r
+       }\r
+\r
+       return results;\r
+};\r
+\r
+Sizzle.error = function( msg ) {\r
+       throw new Error( "Syntax error, unrecognized expression: " + msg );\r
+};\r
+\r
+function tokenize( selector, parseOnly ) {\r
+       var matched, match, tokens, type,\r
+               soFar, groups, preFilters,\r
+               cached = tokenCache[ expando ][ selector + " " ];\r
+\r
+       if ( cached ) {\r
+               return parseOnly ? 0 : cached.slice( 0 );\r
+       }\r
+\r
+       soFar = selector;\r
+       groups = [];\r
+       preFilters = Expr.preFilter;\r
+\r
+       while ( soFar ) {\r
+\r
+               // Comma and first run\r
+               if ( !matched || (match = rcomma.exec( soFar )) ) {\r
+                       if ( match ) {\r
+                               // Don't consume trailing commas as valid\r
+                               soFar = soFar.slice( match[0].length ) || soFar;\r
+                       }\r
+                       groups.push( tokens = [] );\r
+               }\r
+\r
+               matched = false;\r
+\r
+               // Combinators\r
+               if ( (match = rcombinators.exec( soFar )) ) {\r
+                       tokens.push( matched = new Token( match.shift() ) );\r
+                       soFar = soFar.slice( matched.length );\r
+\r
+                       // Cast descendant combinators to space\r
+                       matched.type = match[0].replace( rtrim, " " );\r
+               }\r
+\r
+               // Filters\r
+               for ( type in Expr.filter ) {\r
+                       if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||\r
+                               (match = preFilters[ type ]( match ))) ) {\r
+\r
+                               tokens.push( matched = new Token( match.shift() ) );\r
+                               soFar = soFar.slice( matched.length );\r
+                               matched.type = type;\r
+                               matched.matches = match;\r
+                       }\r
+               }\r
+\r
+               if ( !matched ) {\r
+                       break;\r
+               }\r
+       }\r
+\r
+       // Return the length of the invalid excess\r
+       // if we're just parsing\r
+       // Otherwise, throw an error or return tokens\r
+       return parseOnly ?\r
+               soFar.length :\r
+               soFar ?\r
+                       Sizzle.error( selector ) :\r
+                       // Cache the tokens\r
+                       tokenCache( selector, groups ).slice( 0 );\r
+}\r
+\r
+function addCombinator( matcher, combinator, base ) {\r
+       var dir = combinator.dir,\r
+               checkNonElements = base && combinator.dir === "parentNode",\r
+               doneName = done++;\r
+\r
+       return combinator.first ?\r
+               // Check against closest ancestor/preceding element\r
+               function( elem, context, xml ) {\r
+                       while ( (elem = elem[ dir ]) ) {\r
+                               if ( checkNonElements || elem.nodeType === 1  ) {\r
+                                       return matcher( elem, context, xml );\r
+                               }\r
+                       }\r
+               } :\r
+\r
+               // Check against all ancestor/preceding elements\r
+               function( elem, context, xml ) {\r
+                       // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching\r
+                       if ( !xml ) {\r
+                               var cache,\r
+                                       dirkey = dirruns + " " + doneName + " ",\r
+                                       cachedkey = dirkey + cachedruns;\r
+                               while ( (elem = elem[ dir ]) ) {\r
+                                       if ( checkNonElements || elem.nodeType === 1 ) {\r
+                                               if ( (cache = elem[ expando ]) === cachedkey ) {\r
+                                                       return elem.sizset;\r
+                                               } else if ( typeof cache === "string" && cache.indexOf(dirkey) === 0 ) {\r
+                                                       if ( elem.sizset ) {\r
+                                                               return elem;\r
+                                                       }\r
+                                               } else {\r
+                                                       elem[ expando ] = cachedkey;\r
+                                                       if ( matcher( elem, context, xml ) ) {\r
+                                                               elem.sizset = true;\r
+                                                               return elem;\r
+                                                       }\r
+                                                       elem.sizset = false;\r
+                                               }\r
+                                       }\r
+                               }\r
+                       } else {\r
+                               while ( (elem = elem[ dir ]) ) {\r
+                                       if ( checkNonElements || elem.nodeType === 1 ) {\r
+                                               if ( matcher( elem, context, xml ) ) {\r
+                                                       return elem;\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+               };\r
+}\r
+\r
+function elementMatcher( matchers ) {\r
+       return matchers.length > 1 ?\r
+               function( elem, context, xml ) {\r
+                       var i = matchers.length;\r
+                       while ( i-- ) {\r
+                               if ( !matchers[i]( elem, context, xml ) ) {\r
+                                       return false;\r
+                               }\r
+                       }\r
+                       return true;\r
+               } :\r
+               matchers[0];\r
+}\r
+\r
+function condense( unmatched, map, filter, context, xml ) {\r
+       var elem,\r
+               newUnmatched = [],\r
+               i = 0,\r
+               len = unmatched.length,\r
+               mapped = map != null;\r
+\r
+       for ( ; i < len; i++ ) {\r
+               if ( (elem = unmatched[i]) ) {\r
+                       if ( !filter || filter( elem, context, xml ) ) {\r
+                               newUnmatched.push( elem );\r
+                               if ( mapped ) {\r
+                                       map.push( i );\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+\r
+       return newUnmatched;\r
+}\r
+\r
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\r
+       if ( postFilter && !postFilter[ expando ] ) {\r
+               postFilter = setMatcher( postFilter );\r
+       }\r
+       if ( postFinder && !postFinder[ expando ] ) {\r
+               postFinder = setMatcher( postFinder, postSelector );\r
+       }\r
+       return markFunction(function( seed, results, context, xml ) {\r
+               var temp, i, elem,\r
+                       preMap = [],\r
+                       postMap = [],\r
+                       preexisting = results.length,\r
+\r
+                       // Get initial elements from seed or context\r
+                       elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),\r
+\r
+                       // Prefilter to get matcher input, preserving a map for seed-results synchronization\r
+                       matcherIn = preFilter && ( seed || !selector ) ?\r
+                               condense( elems, preMap, preFilter, context, xml ) :\r
+                               elems,\r
+\r
+                       matcherOut = matcher ?\r
+                               // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,\r
+                               postFinder || ( seed ? preFilter : preexisting || postFilter ) ?\r
+\r
+                                       // ...intermediate processing is necessary\r
+                                       [] :\r
+\r
+                                       // ...otherwise use results directly\r
+                                       results :\r
+                               matcherIn;\r
+\r
+               // Find primary matches\r
+               if ( matcher ) {\r
+                       matcher( matcherIn, matcherOut, context, xml );\r
+               }\r
+\r
+               // Apply postFilter\r
+               if ( postFilter ) {\r
+                       temp = condense( matcherOut, postMap );\r
+                       postFilter( temp, [], context, xml );\r
+\r
+                       // Un-match failing elements by moving them back to matcherIn\r
+                       i = temp.length;\r
+                       while ( i-- ) {\r
+                               if ( (elem = temp[i]) ) {\r
+                                       matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);\r
+                               }\r
+                       }\r
+               }\r
+\r
+               if ( seed ) {\r
+                       if ( postFinder || preFilter ) {\r
+                               if ( postFinder ) {\r
+                                       // Get the final matcherOut by condensing this intermediate into postFinder contexts\r
+                                       temp = [];\r
+                                       i = matcherOut.length;\r
+                                       while ( i-- ) {\r
+                                               if ( (elem = matcherOut[i]) ) {\r
+                                                       // Restore matcherIn since elem is not yet a final match\r
+                                                       temp.push( (matcherIn[i] = elem) );\r
+                                               }\r
+                                       }\r
+                                       postFinder( null, (matcherOut = []), temp, xml );\r
+                               }\r
+\r
+                               // Move matched elements from seed to results to keep them synchronized\r
+                               i = matcherOut.length;\r
+                               while ( i-- ) {\r
+                                       if ( (elem = matcherOut[i]) &&\r
+                                               (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {\r
+\r
+                                               seed[temp] = !(results[temp] = elem);\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+               // Add elements to results, through postFinder if defined\r
+               } else {\r
+                       matcherOut = condense(\r
+                               matcherOut === results ?\r
+                                       matcherOut.splice( preexisting, matcherOut.length ) :\r
+                                       matcherOut\r
+                       );\r
+                       if ( postFinder ) {\r
+                               postFinder( null, results, matcherOut, xml );\r
+                       } else {\r
+                               push.apply( results, matcherOut );\r
+                       }\r
+               }\r
+       });\r
+}\r
+\r
+function matcherFromTokens( tokens ) {\r
+       var checkContext, matcher, j,\r
+               len = tokens.length,\r
+               leadingRelative = Expr.relative[ tokens[0].type ],\r
+               implicitRelative = leadingRelative || Expr.relative[" "],\r
+               i = leadingRelative ? 1 : 0,\r
+\r
+               // The foundational matcher ensures that elements are reachable from top-level context(s)\r
+               matchContext = addCombinator( function( elem ) {\r
+                       return elem === checkContext;\r
+               }, implicitRelative, true ),\r
+               matchAnyContext = addCombinator( function( elem ) {\r
+                       return indexOf.call( checkContext, elem ) > -1;\r
+               }, implicitRelative, true ),\r
+               matchers = [ function( elem, context, xml ) {\r
+                       return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (\r
+                               (checkContext = context).nodeType ?\r
+                                       matchContext( elem, context, xml ) :\r
+                                       matchAnyContext( elem, context, xml ) );\r
+               } ];\r
+\r
+       for ( ; i < len; i++ ) {\r
+               if ( (matcher = Expr.relative[ tokens[i].type ]) ) {\r
+                       matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];\r
+               } else {\r
+                       matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );\r
+\r
+                       // Return special upon seeing a positional matcher\r
+                       if ( matcher[ expando ] ) {\r
+                               // Find the next relative operator (if any) for proper handling\r
+                               j = ++i;\r
+                               for ( ; j < len; j++ ) {\r
+                                       if ( Expr.relative[ tokens[j].type ] ) {\r
+                                               break;\r
+                                       }\r
+                               }\r
+                               return setMatcher(\r
+                                       i > 1 && elementMatcher( matchers ),\r
+                                       i > 1 && tokens.slice( 0, i - 1 ).join("").replace( rtrim, "$1" ),\r
+                                       matcher,\r
+                                       i < j && matcherFromTokens( tokens.slice( i, j ) ),\r
+                                       j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),\r
+                                       j < len && tokens.join("")\r
+                               );\r
+                       }\r
+                       matchers.push( matcher );\r
+               }\r
+       }\r
+\r
+       return elementMatcher( matchers );\r
+}\r
+\r
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {\r
+       var bySet = setMatchers.length > 0,\r
+               byElement = elementMatchers.length > 0,\r
+               superMatcher = function( seed, context, xml, results, expandContext ) {\r
+                       var elem, j, matcher,\r
+                               setMatched = [],\r
+                               matchedCount = 0,\r
+                               i = "0",\r
+                               unmatched = seed && [],\r
+                               outermost = expandContext != null,\r
+                               contextBackup = outermostContext,\r
+                               // We must always have either seed elements or context\r
+                               elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ),\r
+                               // Nested matchers should use non-integer dirruns\r
+                               dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.E);\r
+\r
+                       if ( outermost ) {\r
+                               outermostContext = context !== document && context;\r
+                               cachedruns = superMatcher.el;\r
+                       }\r
+\r
+                       // Add elements passing elementMatchers directly to results\r
+                       for ( ; (elem = elems[i]) != null; i++ ) {\r
+                               if ( byElement && elem ) {\r
+                                       for ( j = 0; (matcher = elementMatchers[j]); j++ ) {\r
+                                               if ( matcher( elem, context, xml ) ) {\r
+                                                       results.push( elem );\r
+                                                       break;\r
+                                               }\r
+                                       }\r
+                                       if ( outermost ) {\r
+                                               dirruns = dirrunsUnique;\r
+                                               cachedruns = ++superMatcher.el;\r
+                                       }\r
+                               }\r
+\r
+                               // Track unmatched elements for set filters\r
+                               if ( bySet ) {\r
+                                       // They will have gone through all possible matchers\r
+                                       if ( (elem = !matcher && elem) ) {\r
+                                               matchedCount--;\r
+                                       }\r
+\r
+                                       // Lengthen the array for every element, matched or not\r
+                                       if ( seed ) {\r
+                                               unmatched.push( elem );\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       // Apply set filters to unmatched elements\r
+                       matchedCount += i;\r
+                       if ( bySet && i !== matchedCount ) {\r
+                               for ( j = 0; (matcher = setMatchers[j]); j++ ) {\r
+                                       matcher( unmatched, setMatched, context, xml );\r
+                               }\r
+\r
+                               if ( seed ) {\r
+                                       // Reintegrate element matches to eliminate the need for sorting\r
+                                       if ( matchedCount > 0 ) {\r
+                                               while ( i-- ) {\r
+                                                       if ( !(unmatched[i] || setMatched[i]) ) {\r
+                                                               setMatched[i] = pop.call( results );\r
+                                                       }\r
+                                               }\r
+                                       }\r
+\r
+                                       // Discard index placeholder values to get only actual matches\r
+                                       setMatched = condense( setMatched );\r
+                               }\r
+\r
+                               // Add matches to results\r
+                               push.apply( results, setMatched );\r
+\r
+                               // Seedless set matches succeeding multiple successful matchers stipulate sorting\r
+                               if ( outermost && !seed && setMatched.length > 0 &&\r
+                                       ( matchedCount + setMatchers.length ) > 1 ) {\r
+\r
+                                       Sizzle.uniqueSort( results );\r
+                               }\r
+                       }\r
+\r
+                       // Override manipulation of globals by nested matchers\r
+                       if ( outermost ) {\r
+                               dirruns = dirrunsUnique;\r
+                               outermostContext = contextBackup;\r
+                       }\r
+\r
+                       return unmatched;\r
+               };\r
+\r
+       superMatcher.el = 0;\r
+       return bySet ?\r
+               markFunction( superMatcher ) :\r
+               superMatcher;\r
+}\r
+\r
+compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {\r
+       var i,\r
+               setMatchers = [],\r
+               elementMatchers = [],\r
+               cached = compilerCache[ expando ][ selector + " " ];\r
+\r
+       if ( !cached ) {\r
+               // Generate a function of recursive functions that can be used to check each element\r
+               if ( !group ) {\r
+                       group = tokenize( selector );\r
+               }\r
+               i = group.length;\r
+               while ( i-- ) {\r
+                       cached = matcherFromTokens( group[i] );\r
+                       if ( cached[ expando ] ) {\r
+                               setMatchers.push( cached );\r
+                       } else {\r
+                               elementMatchers.push( cached );\r
+                       }\r
+               }\r
+\r
+               // Cache the compiled function\r
+               cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );\r
+       }\r
+       return cached;\r
+};\r
+\r
+function multipleContexts( selector, contexts, results ) {\r
+       var i = 0,\r
+               len = contexts.length;\r
+       for ( ; i < len; i++ ) {\r
+               Sizzle( selector, contexts[i], results );\r
+       }\r
+       return results;\r
+}\r
+\r
+function select( selector, context, results, seed, xml ) {\r
+       var i, tokens, token, type, find,\r
+               match = tokenize( selector ),\r
+               j = match.length;\r
+\r
+       if ( !seed ) {\r
+               // Try to minimize operations if there is only one group\r
+               if ( match.length === 1 ) {\r
+\r
+                       // Take a shortcut and set the context if the root selector is an ID\r
+                       tokens = match[0] = match[0].slice( 0 );\r
+                       if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&\r
+                                       context.nodeType === 9 && !xml &&\r
+                                       Expr.relative[ tokens[1].type ] ) {\r
+\r
+                               context = Expr.find["ID"]( token.matches[0].replace( rbackslash, "" ), context, xml )[0];\r
+                               if ( !context ) {\r
+                                       return results;\r
+                               }\r
+\r
+                               selector = selector.slice( tokens.shift().length );\r
+                       }\r
+\r
+                       // Fetch a seed set for right-to-left matching\r
+                       for ( i = matchExpr["POS"].test( selector ) ? -1 : tokens.length - 1; i >= 0; i-- ) {\r
+                               token = tokens[i];\r
+\r
+                               // Abort if we hit a combinator\r
+                               if ( Expr.relative[ (type = token.type) ] ) {\r
+                                       break;\r
+                               }\r
+                               if ( (find = Expr.find[ type ]) ) {\r
+                                       // Search, expanding context for leading sibling combinators\r
+                                       if ( (seed = find(\r
+                                               token.matches[0].replace( rbackslash, "" ),\r
+                                               rsibling.test( tokens[0].type ) && context.parentNode || context,\r
+                                               xml\r
+                                       )) ) {\r
+\r
+                                               // If seed is empty or no tokens remain, we can return early\r
+                                               tokens.splice( i, 1 );\r
+                                               selector = seed.length && tokens.join("");\r
+                                               if ( !selector ) {\r
+                                                       push.apply( results, slice.call( seed, 0 ) );\r
+                                                       return results;\r
+                                               }\r
+\r
+                                               break;\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+\r
+       // Compile and execute a filtering function\r
+       // Provide `match` to avoid retokenization if we modified the selector above\r
+       compile( selector, match )(\r
+               seed,\r
+               context,\r
+               xml,\r
+               results,\r
+               rsibling.test( selector )\r
+       );\r
+       return results;\r
+}\r
+\r
+if ( document.querySelectorAll ) {\r
+       (function() {\r
+               var disconnectedMatch,\r
+                       oldSelect = select,\r
+                       rescape = /'|\\/g,\r
+                       rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,\r
+\r
+                       // qSa(:focus) reports false when true (Chrome 21), no need to also add to buggyMatches since matches checks buggyQSA\r
+                       // A support test would require too much code (would include document ready)\r
+                       rbuggyQSA = [ ":focus" ],\r
+\r
+                       // matchesSelector(:active) reports false when true (IE9/Opera 11.5)\r
+                       // A support test would require too much code (would include document ready)\r
+                       // just skip matchesSelector for :active\r
+                       rbuggyMatches = [ ":active" ],\r
+                       matches = docElem.matchesSelector ||\r
+                               docElem.mozMatchesSelector ||\r
+                               docElem.webkitMatchesSelector ||\r
+                               docElem.oMatchesSelector ||\r
+                               docElem.msMatchesSelector;\r
+\r
+               // Build QSA regex\r
+               // Regex strategy adopted from Diego Perini\r
+               assert(function( div ) {\r
+                       // Select is set to empty string on purpose\r
+                       // This is to test IE's treatment of not explictly\r
+                       // setting a boolean content attribute,\r
+                       // since its presence should be enough\r
+                       // http://bugs.jquery.com/ticket/12359\r
+                       div.innerHTML = "<select><option selected=''></option></select>";\r
+\r
+                       // IE8 - Some boolean attributes are not treated correctly\r
+                       if ( !div.querySelectorAll("[selected]").length ) {\r
+                               rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" );\r
+                       }\r
+\r
+                       // Webkit/Opera - :checked should return selected option elements\r
+                       // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\r
+                       // IE8 throws error here (do not put tests after this one)\r
+                       if ( !div.querySelectorAll(":checked").length ) {\r
+                               rbuggyQSA.push(":checked");\r
+                       }\r
+               });\r
+\r
+               assert(function( div ) {\r
+\r
+                       // Opera 10-12/IE9 - ^= $= *= and empty values\r
+                       // Should not select anything\r
+                       div.innerHTML = "<p test=''></p>";\r
+                       if ( div.querySelectorAll("[test^='']").length ) {\r
+                               rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" );\r
+                       }\r
+\r
+                       // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)\r
+                       // IE8 throws error here (do not put tests after this one)\r
+                       div.innerHTML = "<input type='hidden'/>";\r
+                       if ( !div.querySelectorAll(":enabled").length ) {\r
+                               rbuggyQSA.push(":enabled", ":disabled");\r
+                       }\r
+               });\r
+\r
+               // rbuggyQSA always contains :focus, so no need for a length check\r
+               rbuggyQSA = /* rbuggyQSA.length && */ new RegExp( rbuggyQSA.join("|") );\r
+\r
+               select = function( selector, context, results, seed, xml ) {\r
+                       // Only use querySelectorAll when not filtering,\r
+                       // when this is not xml,\r
+                       // and when no QSA bugs apply\r
+                       if ( !seed && !xml && !rbuggyQSA.test( selector ) ) {\r
+                               var groups, i,\r
+                                       old = true,\r
+                                       nid = expando,\r
+                                       newContext = context,\r
+                                       newSelector = context.nodeType === 9 && selector;\r
+\r
+                               // qSA works strangely on Element-rooted queries\r
+                               // We can work around this by specifying an extra ID on the root\r
+                               // and working up from there (Thanks to Andrew Dupont for the technique)\r
+                               // IE 8 doesn't work on object elements\r
+                               if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {\r
+                                       groups = tokenize( selector );\r
+\r
+                                       if ( (old = context.getAttribute("id")) ) {\r
+                                               nid = old.replace( rescape, "\\$&" );\r
+                                       } else {\r
+                                               context.setAttribute( "id", nid );\r
+                                       }\r
+                                       nid = "[id='" + nid + "'] ";\r
+\r
+                                       i = groups.length;\r
+                                       while ( i-- ) {\r
+                                               groups[i] = nid + groups[i].join("");\r
+                                       }\r
+                                       newContext = rsibling.test( selector ) && context.parentNode || context;\r
+                                       newSelector = groups.join(",");\r
+                               }\r
+\r
+                               if ( newSelector ) {\r
+                                       try {\r
+                                               push.apply( results, slice.call( newContext.querySelectorAll(\r
+                                                       newSelector\r
+                                               ), 0 ) );\r
+                                               return results;\r
+                                       } catch(qsaError) {\r
+                                       } finally {\r
+                                               if ( !old ) {\r
+                                                       context.removeAttribute("id");\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       return oldSelect( selector, context, results, seed, xml );\r
+               };\r
+\r
+               if ( matches ) {\r
+                       assert(function( div ) {\r
+                               // Check to see if it's possible to do matchesSelector\r
+                               // on a disconnected node (IE 9)\r
+                               disconnectedMatch = matches.call( div, "div" );\r
+\r
+                               // This should fail with an exception\r
+                               // Gecko does not error, returns false instead\r
+                               try {\r
+                                       matches.call( div, "[test!='']:sizzle" );\r
+                                       rbuggyMatches.push( "!=", pseudos );\r
+                               } catch ( e ) {}\r
+                       });\r
+\r
+                       // rbuggyMatches always contains :active and :focus, so no need for a length check\r
+                       rbuggyMatches = /* rbuggyMatches.length && */ new RegExp( rbuggyMatches.join("|") );\r
+\r
+                       Sizzle.matchesSelector = function( elem, expr ) {\r
+                               // Make sure that attribute selectors are quoted\r
+                               expr = expr.replace( rattributeQuotes, "='$1']" );\r
+\r
+                               // rbuggyMatches always contains :active, so no need for an existence check\r
+                               if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && !rbuggyQSA.test( expr ) ) {\r
+                                       try {\r
+                                               var ret = matches.call( elem, expr );\r
+\r
+                                               // IE 9's matchesSelector returns false on disconnected nodes\r
+                                               if ( ret || disconnectedMatch ||\r
+                                                               // As well, disconnected nodes are said to be in a document\r
+                                                               // fragment in IE 9\r
+                                                               elem.document && elem.document.nodeType !== 11 ) {\r
+                                                       return ret;\r
+                                               }\r
+                                       } catch(e) {}\r
+                               }\r
+\r
+                               return Sizzle( expr, null, null, [ elem ] ).length > 0;\r
+                       };\r
+               }\r
+       })();\r
+}\r
+\r
+// Deprecated\r
+Expr.pseudos["nth"] = Expr.pseudos["eq"];\r
+\r
+// Back-compat\r
+function setFilters() {}\r
+Expr.filters = setFilters.prototype = Expr.pseudos;\r
+Expr.setFilters = new setFilters();\r
+\r
+// Override sizzle attribute retrieval
+Sizzle.attr = jQuery.attr;
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[":"] = jQuery.expr.pseudos;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+\r
+\r
+})( window );\r
+var runtil = /Until$/,
+       rparentsprev = /^(?:parents|prev(?:Until|All))/,
+       isSimple = /^.[^:#\[\.,]*$/,
+       rneedsContext = jQuery.expr.match.needsContext,
+       // methods guaranteed to produce a unique set when starting from a unique set
+       guaranteedUnique = {
+               children: true,
+               contents: true,
+               next: true,
+               prev: true
+       };
+
+jQuery.fn.extend({
+       find: function( selector ) {
+               var i, l, length, n, r, ret,
+                       self = this;
+
+               if ( typeof selector !== "string" ) {
+                       return jQuery( selector ).filter(function() {
+                               for ( i = 0, l = self.length; i < l; i++ ) {
+                                       if ( jQuery.contains( self[ i ], this ) ) {
+                                               return true;
+                                       }
+                               }
+                       });
+               }
+
+               ret = this.pushStack( "", "find", selector );
+
+               for ( i = 0, l = this.length; i < l; i++ ) {
+                       length = ret.length;
+                       jQuery.find( selector, this[i], ret );
+
+                       if ( i > 0 ) {
+                               // Make sure that the results are unique
+                               for ( n = length; n < ret.length; n++ ) {
+                                       for ( r = 0; r < length; r++ ) {
+                                               if ( ret[r] === ret[n] ) {
+                                                       ret.splice(n--, 1);
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               return ret;
+       },
+
+       has: function( target ) {
+               var i,
+                       targets = jQuery( target, this ),
+                       len = targets.length;
+
+               return this.filter(function() {
+                       for ( i = 0; i < len; i++ ) {
+                               if ( jQuery.contains( this, targets[i] ) ) {
+                                       return true;
+                               }
+                       }
+               });
+       },
+
+       not: function( selector ) {
+               return this.pushStack( winnow(this, selector, false), "not", selector);
+       },
+
+       filter: function( selector ) {
+               return this.pushStack( winnow(this, selector, true), "filter", selector );
+       },
+
+       is: function( selector ) {
+               return !!selector && (
+                       typeof selector === "string" ?
+                               // If this is a positional/relative selector, check membership in the returned set
+                               // so $("p:first").is("p:last") won't return true for a doc with two "p".
+                               rneedsContext.test( selector ) ?
+                                       jQuery( selector, this.context ).index( this[0] ) >= 0 :
+                                       jQuery.filter( selector, this ).length > 0 :
+                               this.filter( selector ).length > 0 );
+       },
+
+       closest: function( selectors, context ) {
+               var cur,
+                       i = 0,
+                       l = this.length,
+                       ret = [],
+                       pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
+                               jQuery( selectors, context || this.context ) :
+                               0;
+
+               for ( ; i < l; i++ ) {
+                       cur = this[i];
+
+                       while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) {
+                               if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {
+                                       ret.push( cur );
+                                       break;
+                               }
+                               cur = cur.parentNode;
+                       }
+               }
+
+               ret = ret.length > 1 ? jQuery.unique( ret ) : ret;
+
+               return this.pushStack( ret, "closest", selectors );
+       },
+
+       // Determine the position of an element within
+       // the matched set of elements
+       index: function( elem ) {
+
+               // No argument, return index in parent
+               if ( !elem ) {
+                       return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1;
+               }
+
+               // index in selector
+               if ( typeof elem === "string" ) {
+                       return jQuery.inArray( this[0], jQuery( elem ) );
+               }
+
+               // Locate the position of the desired element
+               return jQuery.inArray(
+                       // If it receives a jQuery object, the first element is used
+                       elem.jquery ? elem[0] : elem, this );
+       },
+
+       add: function( selector, context ) {
+               var set = typeof selector === "string" ?
+                               jQuery( selector, context ) :
+                               jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
+                       all = jQuery.merge( this.get(), set );
+
+               return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
+                       all :
+                       jQuery.unique( all ) );
+       },
+
+       addBack: function( selector ) {
+               return this.add( selector == null ?
+                       this.prevObject : this.prevObject.filter(selector)
+               );
+       }
+});
+
+jQuery.fn.andSelf = jQuery.fn.addBack;
+
+// A painfully simple check to see if an element is disconnected
+// from a document (should be improved, where feasible).
+function isDisconnected( node ) {
+       return !node || !node.parentNode || node.parentNode.nodeType === 11;
+}
+
+function sibling( cur, dir ) {
+       do {
+               cur = cur[ dir ];
+       } while ( cur && cur.nodeType !== 1 );
+
+       return cur;
+}
+
+jQuery.each({
+       parent: function( elem ) {
+               var parent = elem.parentNode;
+               return parent && parent.nodeType !== 11 ? parent : null;
+       },
+       parents: function( elem ) {
+               return jQuery.dir( elem, "parentNode" );
+       },
+       parentsUntil: function( elem, i, until ) {
+               return jQuery.dir( elem, "parentNode", until );
+       },
+       next: function( elem ) {
+               return sibling( elem, "nextSibling" );
+       },
+       prev: function( elem ) {
+               return sibling( elem, "previousSibling" );
+       },
+       nextAll: function( elem ) {
+               return jQuery.dir( elem, "nextSibling" );
+       },
+       prevAll: function( elem ) {
+               return jQuery.dir( elem, "previousSibling" );
+       },
+       nextUntil: function( elem, i, until ) {
+               return jQuery.dir( elem, "nextSibling", until );
+       },
+       prevUntil: function( elem, i, until ) {
+               return jQuery.dir( elem, "previousSibling", until );
+       },
+       siblings: function( elem ) {
+               return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
+       },
+       children: function( elem ) {
+               return jQuery.sibling( elem.firstChild );
+       },
+       contents: function( elem ) {
+               return jQuery.nodeName( elem, "iframe" ) ?
+                       elem.contentDocument || elem.contentWindow.document :
+                       jQuery.merge( [], elem.childNodes );
+       }
+}, function( name, fn ) {
+       jQuery.fn[ name ] = function( until, selector ) {
+               var ret = jQuery.map( this, fn, until );
+
+               if ( !runtil.test( name ) ) {
+                       selector = until;
+               }
+
+               if ( selector && typeof selector === "string" ) {
+                       ret = jQuery.filter( selector, ret );
+               }
+
+               ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+               if ( this.length > 1 && rparentsprev.test( name ) ) {
+                       ret = ret.reverse();
+               }
+
+               return this.pushStack( ret, name, core_slice.call( arguments ).join(",") );
+       };
+});
+
+jQuery.extend({
+       filter: function( expr, elems, not ) {
+               if ( not ) {
+                       expr = ":not(" + expr + ")";
+               }
+
+               return elems.length === 1 ?
+                       jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
+                       jQuery.find.matches(expr, elems);
+       },
+
+       dir: function( elem, dir, until ) {
+               var matched = [],
+                       cur = elem[ dir ];
+
+               while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+                       if ( cur.nodeType === 1 ) {
+                               matched.push( cur );
+                       }
+                       cur = cur[dir];
+               }
+               return matched;
+       },
+
+       sibling: function( n, elem ) {
+               var r = [];
+
+               for ( ; n; n = n.nextSibling ) {
+                       if ( n.nodeType === 1 && n !== elem ) {
+                               r.push( n );
+                       }
+               }
+
+               return r;
+       }
+});
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, keep ) {
+
+       // Can't pass null or undefined to indexOf in Firefox 4
+       // Set to 0 to skip string check
+       qualifier = qualifier || 0;
+
+       if ( jQuery.isFunction( qualifier ) ) {
+               return jQuery.grep(elements, function( elem, i ) {
+                       var retVal = !!qualifier.call( elem, i, elem );
+                       return retVal === keep;
+               });
+
+       } else if ( qualifier.nodeType ) {
+               return jQuery.grep(elements, function( elem, i ) {
+                       return ( elem === qualifier ) === keep;
+               });
+
+       } else if ( typeof qualifier === "string" ) {
+               var filtered = jQuery.grep(elements, function( elem ) {
+                       return elem.nodeType === 1;
+               });
+
+               if ( isSimple.test( qualifier ) ) {
+                       return jQuery.filter(qualifier, filtered, !keep);
+               } else {
+                       qualifier = jQuery.filter( qualifier, filtered );
+               }
+       }
+
+       return jQuery.grep(elements, function( elem, i ) {
+               return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep;
+       });
+}
+function createSafeFragment( document ) {
+       var list = nodeNames.split( "|" ),
+       safeFrag = document.createDocumentFragment();
+
+       if ( safeFrag.createElement ) {
+               while ( list.length ) {
+                       safeFrag.createElement(
+                               list.pop()
+                       );
+               }
+       }
+       return safeFrag;
+}
+
+var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
+               "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
+       rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g,
+       rleadingWhitespace = /^\s+/,
+       rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
+       rtagName = /<([\w:]+)/,
+       rtbody = /<tbody/i,
+       rhtml = /<|&#?\w+;/,
+       rnoInnerhtml = /<(?:script|style|link)/i,
+       rnocache = /<(?:script|object|embed|option|style)/i,
+       rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"),
+       rcheckableType = /^(?:checkbox|radio)$/,
+       // checked="checked" or checked
+       rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+       rscriptType = /\/(java|ecma)script/i,
+       rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,
+       wrapMap = {
+               option: [ 1, "<select multiple='multiple'>", "</select>" ],
+               legend: [ 1, "<fieldset>", "</fieldset>" ],
+               thead: [ 1, "<table>", "</table>" ],
+               tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+               td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+               col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+               area: [ 1, "<map>", "</map>" ],
+               _default: [ 0, "", "" ]
+       },
+       safeFragment = createSafeFragment( document ),
+       fragmentDiv = safeFragment.appendChild( document.createElement("div") );
+
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
+// unless wrapped in a div with non-breaking characters in front of it.
+if ( !jQuery.support.htmlSerialize ) {
+       wrapMap._default = [ 1, "X<div>", "</div>" ];
+}
+
+jQuery.fn.extend({
+       text: function( value ) {
+               return jQuery.access( this, function( value ) {
+                       return value === undefined ?
+                               jQuery.text( this ) :
+                               this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );
+               }, null, value, arguments.length );
+       },
+
+       wrapAll: function( html ) {
+               if ( jQuery.isFunction( html ) ) {
+                       return this.each(function(i) {
+                               jQuery(this).wrapAll( html.call(this, i) );
+                       });
+               }
+
+               if ( this[0] ) {
+                       // The elements to wrap the target around
+                       var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+                       if ( this[0].parentNode ) {
+                               wrap.insertBefore( this[0] );
+                       }
+
+                       wrap.map(function() {
+                               var elem = this;
+
+                               while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+                                       elem = elem.firstChild;
+                               }
+
+                               return elem;
+                       }).append( this );
+               }
+
+               return this;
+       },
+
+       wrapInner: function( html ) {
+               if ( jQuery.isFunction( html ) ) {
+                       return this.each(function(i) {
+                               jQuery(this).wrapInner( html.call(this, i) );
+                       });
+               }
+
+               return this.each(function() {
+                       var self = jQuery( this ),
+                               contents = self.contents();
+
+                       if ( contents.length ) {
+                               contents.wrapAll( html );
+
+                       } else {
+                               self.append( html );
+                       }
+               });
+       },
+
+       wrap: function( html ) {
+               var isFunction = jQuery.isFunction( html );
+
+               return this.each(function(i) {
+                       jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
+               });
+       },
+
+       unwrap: function() {
+               return this.parent().each(function() {
+                       if ( !jQuery.nodeName( this, "body" ) ) {
+                               jQuery( this ).replaceWith( this.childNodes );
+                       }
+               }).end();
+       },
+
+       append: function() {
+               return this.domManip(arguments, true, function( elem ) {
+                       if ( this.nodeType === 1 || this.nodeType === 11 ) {
+                               this.appendChild( elem );
+                       }
+               });
+       },
+
+       prepend: function() {
+               return this.domManip(arguments, true, function( elem ) {
+                       if ( this.nodeType === 1 || this.nodeType === 11 ) {
+                               this.insertBefore( elem, this.firstChild );
+                       }
+               });
+       },
+
+       before: function() {
+               if ( !isDisconnected( this[0] ) ) {
+                       return this.domManip(arguments, false, function( elem ) {
+                               this.parentNode.insertBefore( elem, this );
+                       });
+               }
+
+               if ( arguments.length ) {
+                       var set = jQuery.clean( arguments );
+                       return this.pushStack( jQuery.merge( set, this ), "before", this.selector );
+               }
+       },
+
+       after: function() {
+               if ( !isDisconnected( this[0] ) ) {
+                       return this.domManip(arguments, false, function( elem ) {
+                               this.parentNode.insertBefore( elem, this.nextSibling );
+                       });
+               }
+
+               if ( arguments.length ) {
+                       var set = jQuery.clean( arguments );
+                       return this.pushStack( jQuery.merge( this, set ), "after", this.selector );
+               }
+       },
+
+       // keepData is for internal use only--do not document
+       remove: function( selector, keepData ) {
+               var elem,
+                       i = 0;
+
+               for ( ; (elem = this[i]) != null; i++ ) {
+                       if ( !selector || jQuery.filter( selector, [ elem ] ).length ) {
+                               if ( !keepData && elem.nodeType === 1 ) {
+                                       jQuery.cleanData( elem.getElementsByTagName("*") );
+                                       jQuery.cleanData( [ elem ] );
+                               }
+
+                               if ( elem.parentNode ) {
+                                       elem.parentNode.removeChild( elem );
+                               }
+                       }
+               }
+
+               return this;
+       },
+
+       empty: function() {
+               var elem,
+                       i = 0;
+
+               for ( ; (elem = this[i]) != null; i++ ) {
+                       // Remove element nodes and prevent memory leaks
+                       if ( elem.nodeType === 1 ) {
+                               jQuery.cleanData( elem.getElementsByTagName("*") );
+                       }
+
+                       // Remove any remaining nodes
+                       while ( elem.firstChild ) {
+                               elem.removeChild( elem.firstChild );
+                       }
+               }
+
+               return this;
+       },
+
+       clone: function( dataAndEvents, deepDataAndEvents ) {
+               dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+               deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+               return this.map( function () {
+                       return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+               });
+       },
+
+       html: function( value ) {
+               return jQuery.access( this, function( value ) {
+                       var elem = this[0] || {},
+                               i = 0,
+                               l = this.length;
+
+                       if ( value === undefined ) {
+                               return elem.nodeType === 1 ?
+                                       elem.innerHTML.replace( rinlinejQuery, "" ) :
+                                       undefined;
+                       }
+
+                       // See if we can take a shortcut and just use innerHTML
+                       if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+                               ( jQuery.support.htmlSerialize || !rnoshimcache.test( value )  ) &&
+                               ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&
+                               !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) {
+
+                               value = value.replace( rxhtmlTag, "<$1></$2>" );
+
+                               try {
+                                       for (; i < l; i++ ) {
+                                               // Remove element nodes and prevent memory leaks
+                                               elem = this[i] || {};
+                                               if ( elem.nodeType === 1 ) {
+                                                       jQuery.cleanData( elem.getElementsByTagName( "*" ) );
+                                                       elem.innerHTML = value;
+                                               }
+                                       }
+
+                                       elem = 0;
+
+                               // If using innerHTML throws an exception, use the fallback method
+                               } catch(e) {}
+                       }
+
+                       if ( elem ) {
+                               this.empty().append( value );
+                       }
+               }, null, value, arguments.length );
+       },
+
+       replaceWith: function( value ) {
+               if ( !isDisconnected( this[0] ) ) {
+                       // Make sure that the elements are removed from the DOM before they are inserted
+                       // this can help fix replacing a parent with child elements
+                       if ( jQuery.isFunction( value ) ) {
+                               return this.each(function(i) {
+                                       var self = jQuery(this), old = self.html();
+                                       self.replaceWith( value.call( this, i, old ) );
+                               });
+                       }
+
+                       if ( typeof value !== "string" ) {
+                               value = jQuery( value ).detach();
+                       }
+
+                       return this.each(function() {
+                               var next = this.nextSibling,
+                                       parent = this.parentNode;
+
+                               jQuery( this ).remove();
+
+                               if ( next ) {
+                                       jQuery(next).before( value );
+                               } else {
+                                       jQuery(parent).append( value );
+                               }
+                       });
+               }
+
+               return this.length ?
+                       this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) :
+                       this;
+       },
+
+       detach: function( selector ) {
+               return this.remove( selector, true );
+       },
+
+       domManip: function( args, table, callback ) {
+
+               // Flatten any nested arrays
+               args = [].concat.apply( [], args );
+
+               var results, first, fragment, iNoClone,
+                       i = 0,
+                       value = args[0],
+                       scripts = [],
+                       l = this.length;
+
+               // We can't cloneNode fragments that contain checked, in WebKit
+               if ( !jQuery.support.checkClone && l > 1 && typeof value === "string" && rchecked.test( value ) ) {
+                       return this.each(function() {
+                               jQuery(this).domManip( args, table, callback );
+                       });
+               }
+
+               if ( jQuery.isFunction(value) ) {
+                       return this.each(function(i) {
+                               var self = jQuery(this);
+                               args[0] = value.call( this, i, table ? self.html() : undefined );
+                               self.domManip( args, table, callback );
+                       });
+               }
+
+               if ( this[0] ) {
+                       results = jQuery.buildFragment( args, this, scripts );
+                       fragment = results.fragment;
+                       first = fragment.firstChild;
+
+                       if ( fragment.childNodes.length === 1 ) {
+                               fragment = first;
+                       }
+
+                       if ( first ) {
+                               table = table && jQuery.nodeName( first, "tr" );
+
+                               // Use the original fragment for the last item instead of the first because it can end up
+                               // being emptied incorrectly in certain situations (#8070).
+                               // Fragments from the fragment cache must always be cloned and never used in place.
+                               for ( iNoClone = results.cacheable || l - 1; i < l; i++ ) {
+                                       callback.call(
+                                               table && jQuery.nodeName( this[i], "table" ) ?
+                                                       findOrAppend( this[i], "tbody" ) :
+                                                       this[i],
+                                               i === iNoClone ?
+                                                       fragment :
+                                                       jQuery.clone( fragment, true, true )
+                                       );
+                               }
+                       }
+
+                       // Fix #11809: Avoid leaking memory
+                       fragment = first = null;
+
+                       if ( scripts.length ) {
+                               jQuery.each( scripts, function( i, elem ) {
+                                       if ( elem.src ) {
+                                               if ( jQuery.ajax ) {
+                                                       jQuery.ajax({
+                                                               url: elem.src,
+                                                               type: "GET",
+                                                               dataType: "script",
+                                                               async: false,
+                                                               global: false,
+                                                               "throws": true
+                                                       });
+                                               } else {
+                                                       jQuery.error("no ajax");
+                                               }
+                                       } else {
+                                               jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "" ) );
+                                       }
+
+                                       if ( elem.parentNode ) {
+                                               elem.parentNode.removeChild( elem );
+                                       }
+                               });
+                       }
+               }
+
+               return this;
+       }
+});
+
+function findOrAppend( elem, tag ) {
+       return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) );
+}
+
+function cloneCopyEvent( src, dest ) {
+
+       if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
+               return;
+       }
+
+       var type, i, l,
+               oldData = jQuery._data( src ),
+               curData = jQuery._data( dest, oldData ),
+               events = oldData.events;
+
+       if ( events ) {
+               delete curData.handle;
+               curData.events = {};
+
+               for ( type in events ) {
+                       for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+                               jQuery.event.add( dest, type, events[ type ][ i ] );
+                       }
+               }
+       }
+
+       // make the cloned public data object a copy from the original
+       if ( curData.data ) {
+               curData.data = jQuery.extend( {}, curData.data );
+       }
+}
+
+function cloneFixAttributes( src, dest ) {
+       var nodeName;
+
+       // We do not need to do anything for non-Elements
+       if ( dest.nodeType !== 1 ) {
+               return;
+       }
+
+       // clearAttributes removes the attributes, which we don't want,
+       // but also removes the attachEvent events, which we *do* want
+       if ( dest.clearAttributes ) {
+               dest.clearAttributes();
+       }
+
+       // mergeAttributes, in contrast, only merges back on the
+       // original attributes, not the events
+       if ( dest.mergeAttributes ) {
+               dest.mergeAttributes( src );
+       }
+
+       nodeName = dest.nodeName.toLowerCase();
+
+       if ( nodeName === "object" ) {
+               // IE6-10 improperly clones children of object elements using classid.
+               // IE10 throws NoModificationAllowedError if parent is null, #12132.
+               if ( dest.parentNode ) {
+                       dest.outerHTML = src.outerHTML;
+               }
+
+               // This path appears unavoidable for IE9. When cloning an object
+               // element in IE9, the outerHTML strategy above is not sufficient.
+               // If the src has innerHTML and the destination does not,
+               // copy the src.innerHTML into the dest.innerHTML. #10324
+               if ( jQuery.support.html5Clone && (src.innerHTML && !jQuery.trim(dest.innerHTML)) ) {
+                       dest.innerHTML = src.innerHTML;
+               }
+
+       } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
+               // IE6-8 fails to persist the checked state of a cloned checkbox
+               // or radio button. Worse, IE6-7 fail to give the cloned element
+               // a checked appearance if the defaultChecked value isn't also set
+
+               dest.defaultChecked = dest.checked = src.checked;
+
+               // IE6-7 get confused and end up setting the value of a cloned
+               // checkbox/radio button to an empty string instead of "on"
+               if ( dest.value !== src.value ) {
+                       dest.value = src.value;
+               }
+
+       // IE6-8 fails to return the selected option to the default selected
+       // state when cloning options
+       } else if ( nodeName === "option" ) {
+               dest.selected = src.defaultSelected;
+
+       // IE6-8 fails to set the defaultValue to the correct value when
+       // cloning other types of input fields
+       } else if ( nodeName === "input" || nodeName === "textarea" ) {
+               dest.defaultValue = src.defaultValue;
+
+       // IE blanks contents when cloning scripts
+       } else if ( nodeName === "script" && dest.text !== src.text ) {
+               dest.text = src.text;
+       }
+
+       // Event data gets referenced instead of copied if the expando
+       // gets copied too
+       dest.removeAttribute( jQuery.expando );
+}
+
+jQuery.buildFragment = function( args, context, scripts ) {
+       var fragment, cacheable, cachehit,
+               first = args[ 0 ];
+
+       // Set context from what may come in as undefined or a jQuery collection or a node
+       // Updated to fix #12266 where accessing context[0] could throw an exception in IE9/10 &
+       // also doubles as fix for #8950 where plain objects caused createDocumentFragment exception
+       context = context || document;
+       context = !context.nodeType && context[0] || context;
+       context = context.ownerDocument || context;
+
+       // Only cache "small" (1/2 KB) HTML strings that are associated with the main document
+       // Cloning options loses the selected state, so don't cache them
+       // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
+       // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache
+       // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501
+       if ( args.length === 1 && typeof first === "string" && first.length < 512 && context === document &&
+               first.charAt(0) === "<" && !rnocache.test( first ) &&
+               (jQuery.support.checkClone || !rchecked.test( first )) &&
+               (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) {
+
+               // Mark cacheable and look for a hit
+               cacheable = true;
+               fragment = jQuery.fragments[ first ];
+               cachehit = fragment !== undefined;
+       }
+
+       if ( !fragment ) {
+               fragment = context.createDocumentFragment();
+               jQuery.clean( args, context, fragment, scripts );
+
+               // Update the cache, but only store false
+               // unless this is a second parsing of the same content
+               if ( cacheable ) {
+                       jQuery.fragments[ first ] = cachehit && fragment;
+               }
+       }
+
+       return { fragment: fragment, cacheable: cacheable };
+};
+
+jQuery.fragments = {};
+
+jQuery.each({
+       appendTo: "append",
+       prependTo: "prepend",
+       insertBefore: "before",
+       insertAfter: "after",
+       replaceAll: "replaceWith"
+}, function( name, original ) {
+       jQuery.fn[ name ] = function( selector ) {
+               var elems,
+                       i = 0,
+                       ret = [],
+                       insert = jQuery( selector ),
+                       l = insert.length,
+                       parent = this.length === 1 && this[0].parentNode;
+
+               if ( (parent == null || parent && parent.nodeType === 11 && parent.childNodes.length === 1) && l === 1 ) {
+                       insert[ original ]( this[0] );
+                       return this;
+               } else {
+                       for ( ; i < l; i++ ) {
+                               elems = ( i > 0 ? this.clone(true) : this ).get();
+                               jQuery( insert[i] )[ original ]( elems );
+                               ret = ret.concat( elems );
+                       }
+
+                       return this.pushStack( ret, name, insert.selector );
+               }
+       };
+});
+
+function getAll( elem ) {
+       if ( typeof elem.getElementsByTagName !== "undefined" ) {
+               return elem.getElementsByTagName( "*" );
+
+       } else if ( typeof elem.querySelectorAll !== "undefined" ) {
+               return elem.querySelectorAll( "*" );
+
+       } else {
+               return [];
+       }
+}
+
+// Used in clean, fixes the defaultChecked property
+function fixDefaultChecked( elem ) {
+       if ( rcheckableType.test( elem.type ) ) {
+               elem.defaultChecked = elem.checked;
+       }
+}
+
+jQuery.extend({
+       clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+               var srcElements,
+                       destElements,
+                       i,
+                       clone;
+
+               if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
+                       clone = elem.cloneNode( true );
+
+               // IE<=8 does not properly clone detached, unknown element nodes
+               } else {
+                       fragmentDiv.innerHTML = elem.outerHTML;
+                       fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
+               }
+
+               if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
+                               (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
+                       // IE copies events bound via attachEvent when using cloneNode.
+                       // Calling detachEvent on the clone will also remove the events
+                       // from the original. In order to get around this, we use some
+                       // proprietary methods to clear the events. Thanks to MooTools
+                       // guys for this hotness.
+
+                       cloneFixAttributes( elem, clone );
+
+                       // Using Sizzle here is crazy slow, so we use getElementsByTagName instead
+                       srcElements = getAll( elem );
+                       destElements = getAll( clone );
+
+                       // Weird iteration because IE will replace the length property
+                       // with an element if you are cloning the body and one of the
+                       // elements on the page has a name or id of "length"
+                       for ( i = 0; srcElements[i]; ++i ) {
+                               // Ensure that the destination node is not null; Fixes #9587
+                               if ( destElements[i] ) {
+                                       cloneFixAttributes( srcElements[i], destElements[i] );
+                               }
+                       }
+               }
+
+               // Copy the events from the original to the clone
+               if ( dataAndEvents ) {
+                       cloneCopyEvent( elem, clone );
+
+                       if ( deepDataAndEvents ) {
+                               srcElements = getAll( elem );
+                               destElements = getAll( clone );
+
+                               for ( i = 0; srcElements[i]; ++i ) {
+                                       cloneCopyEvent( srcElements[i], destElements[i] );
+                               }
+                       }
+               }
+
+               srcElements = destElements = null;
+
+               // Return the cloned set
+               return clone;
+       },
+
+       clean: function( elems, context, fragment, scripts ) {
+               var i, j, elem, tag, wrap, depth, div, hasBody, tbody, len, handleScript, jsTags,
+                       safe = context === document && safeFragment,
+                       ret = [];
+
+               // Ensure that context is a document
+               if ( !context || typeof context.createDocumentFragment === "undefined" ) {
+                       context = document;
+               }
+
+               // Use the already-created safe fragment if context permits
+               for ( i = 0; (elem = elems[i]) != null; i++ ) {
+                       if ( typeof elem === "number" ) {
+                               elem += "";
+                       }
+
+                       if ( !elem ) {
+                               continue;
+                       }
+
+                       // Convert html string into DOM nodes
+                       if ( typeof elem === "string" ) {
+                               if ( !rhtml.test( elem ) ) {
+                                       elem = context.createTextNode( elem );
+                               } else {
+                                       // Ensure a safe container in which to render the html
+                                       safe = safe || createSafeFragment( context );
+                                       div = context.createElement("div");
+                                       safe.appendChild( div );
+
+                                       // Fix "XHTML"-style tags in all browsers
+                                       elem = elem.replace(rxhtmlTag, "<$1></$2>");
+
+                                       // Go to html and back, then peel off extra wrappers
+                                       tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase();
+                                       wrap = wrapMap[ tag ] || wrapMap._default;
+                                       depth = wrap[0];
+                                       div.innerHTML = wrap[1] + elem + wrap[2];
+
+                                       // Move to the right depth
+                                       while ( depth-- ) {
+                                               div = div.lastChild;
+                                       }
+
+                                       // Remove IE's autoinserted <tbody> from table fragments
+                                       if ( !jQuery.support.tbody ) {
+
+                                               // String was a <table>, *may* have spurious <tbody>
+                                               hasBody = rtbody.test(elem);
+                                                       tbody = tag === "table" && !hasBody ?
+                                                               div.firstChild && div.firstChild.childNodes :
+
+                                                               // String was a bare <thead> or <tfoot>
+                                                               wrap[1] === "<table>" && !hasBody ?
+                                                                       div.childNodes :
+                                                                       [];
+
+                                               for ( j = tbody.length - 1; j >= 0 ; --j ) {
+                                                       if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
+                                                               tbody[ j ].parentNode.removeChild( tbody[ j ] );
+                                                       }
+                                               }
+                                       }
+
+                                       // IE completely kills leading whitespace when innerHTML is used
+                                       if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+                                               div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
+                                       }
+
+                                       elem = div.childNodes;
+
+                                       // Take out of fragment container (we need a fresh div each time)
+                                       div.parentNode.removeChild( div );
+                               }
+                       }
+
+                       if ( elem.nodeType ) {
+                               ret.push( elem );
+                       } else {
+                               jQuery.merge( ret, elem );
+                       }
+               }
+
+               // Fix #11356: Clear elements from safeFragment
+               if ( div ) {
+                       elem = div = safe = null;
+               }
+
+               // Reset defaultChecked for any radios and checkboxes
+               // about to be appended to the DOM in IE 6/7 (#8060)
+               if ( !jQuery.support.appendChecked ) {
+                       for ( i = 0; (elem = ret[i]) != null; i++ ) {
+                               if ( jQuery.nodeName( elem, "input" ) ) {
+                                       fixDefaultChecked( elem );
+                               } else if ( typeof elem.getElementsByTagName !== "undefined" ) {
+                                       jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked );
+                               }
+                       }
+               }
+
+               // Append elements to a provided document fragment
+               if ( fragment ) {
+                       // Special handling of each script element
+                       handleScript = function( elem ) {
+                               // Check if we consider it executable
+                               if ( !elem.type || rscriptType.test( elem.type ) ) {
+                                       // Detach the script and store it in the scripts array (if provided) or the fragment
+                                       // Return truthy to indicate that it has been handled
+                                       return scripts ?
+                                               scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) :
+                                               fragment.appendChild( elem );
+                               }
+                       };
+
+                       for ( i = 0; (elem = ret[i]) != null; i++ ) {
+                               // Check if we're done after handling an executable script
+                               if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) {
+                                       // Append to fragment and handle embedded scripts
+                                       fragment.appendChild( elem );
+                                       if ( typeof elem.getElementsByTagName !== "undefined" ) {
+                                               // handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration
+                                               jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript );
+
+                                               // Splice the scripts into ret after their former ancestor and advance our index beyond them
+                                               ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
+                                               i += jsTags.length;
+                                       }
+                               }
+                       }
+               }
+
+               return ret;
+       },
+
+       cleanData: function( elems, /* internal */ acceptData ) {
+               var data, id, elem, type,
+                       i = 0,
+                       internalKey = jQuery.expando,
+                       cache = jQuery.cache,
+                       deleteExpando = jQuery.support.deleteExpando,
+                       special = jQuery.event.special;
+
+               for ( ; (elem = elems[i]) != null; i++ ) {
+
+                       if ( acceptData || jQuery.acceptData( elem ) ) {
+
+                               id = elem[ internalKey ];
+                               data = id && cache[ id ];
+
+                               if ( data ) {
+                                       if ( data.events ) {
+                                               for ( type in data.events ) {
+                                                       if ( special[ type ] ) {
+                                                               jQuery.event.remove( elem, type );
+
+                                                       // This is a shortcut to avoid jQuery.event.remove's overhead
+                                                       } else {
+                                                               jQuery.removeEvent( elem, type, data.handle );
+                                                       }
+                                               }
+                                       }
+
+                                       // Remove cache only if it was not already removed by jQuery.event.remove
+                                       if ( cache[ id ] ) {
+
+                                               delete cache[ id ];
+
+                                               // IE does not allow us to delete expando properties from nodes,
+                                               // nor does it have a removeAttribute function on Document nodes;
+                                               // we must handle all of these cases
+                                               if ( deleteExpando ) {
+                                                       delete elem[ internalKey ];
+
+                                               } else if ( elem.removeAttribute ) {
+                                                       elem.removeAttribute( internalKey );
+
+                                               } else {
+                                                       elem[ internalKey ] = null;
+                                               }
+
+                                               jQuery.deletedIds.push( id );
+                                       }
+                               }
+                       }
+               }
+       }
+});
+// Limit scope pollution from any deprecated API
+(function() {
+
+var matched, browser;
+
+// Use of jQuery.browser is frowned upon.
+// More details: http://api.jquery.com/jQuery.browser
+// jQuery.uaMatch maintained for back-compat
+jQuery.uaMatch = function( ua ) {
+       ua = ua.toLowerCase();
+
+       var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
+               /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
+               /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
+               /(msie) ([\w.]+)/.exec( ua ) ||
+               ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
+               [];
+
+       return {
+               browser: match[ 1 ] || "",
+               version: match[ 2 ] || "0"
+       };
+};
+
+matched = jQuery.uaMatch( navigator.userAgent );
+browser = {};
+
+if ( matched.browser ) {
+       browser[ matched.browser ] = true;
+       browser.version = matched.version;
+}
+
+// Chrome is Webkit, but Webkit is also Safari.
+if ( browser.chrome ) {
+       browser.webkit = true;
+} else if ( browser.webkit ) {
+       browser.safari = true;
+}
+
+jQuery.browser = browser;
+
+jQuery.sub = function() {
+       function jQuerySub( selector, context ) {
+               return new jQuerySub.fn.init( selector, context );
+       }
+       jQuery.extend( true, jQuerySub, this );
+       jQuerySub.superclass = this;
+       jQuerySub.fn = jQuerySub.prototype = this();
+       jQuerySub.fn.constructor = jQuerySub;
+       jQuerySub.sub = this.sub;
+       jQuerySub.fn.init = function init( selector, context ) {
+               if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
+                       context = jQuerySub( context );
+               }
+
+               return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
+       };
+       jQuerySub.fn.init.prototype = jQuerySub.fn;
+       var rootjQuerySub = jQuerySub(document);
+       return jQuerySub;
+};
+
+})();
+var curCSS, iframe, iframeDoc,
+       ralpha = /alpha\([^)]*\)/i,
+       ropacity = /opacity=([^)]*)/,
+       rposition = /^(top|right|bottom|left)$/,
+       // swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
+       // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+       rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+       rmargin = /^margin/,
+       rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ),
+       rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ),
+       rrelNum = new RegExp( "^([-+])=(" + core_pnum + ")", "i" ),
+       elemdisplay = { BODY: "block" },
+
+       cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+       cssNormalTransform = {
+               letterSpacing: 0,
+               fontWeight: 400
+       },
+
+       cssExpand = [ "Top", "Right", "Bottom", "Left" ],
+       cssPrefixes = [ "Webkit", "O", "Moz", "ms" ],
+
+       eventsToggle = jQuery.fn.toggle;
+
+// return a css property mapped to a potentially vendor prefixed property
+function vendorPropName( style, name ) {
+
+       // shortcut for names that are not vendor prefixed
+       if ( name in style ) {
+               return name;
+       }
+
+       // check for vendor prefixed names
+       var capName = name.charAt(0).toUpperCase() + name.slice(1),
+               origName = name,
+               i = cssPrefixes.length;
+
+       while ( i-- ) {
+               name = cssPrefixes[ i ] + capName;
+               if ( name in style ) {
+                       return name;
+               }
+       }
+
+       return origName;
+}
+
+function isHidden( elem, el ) {
+       elem = el || elem;
+       return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
+}
+
+function showHide( elements, show ) {
+       var elem, display,
+               values = [],
+               index = 0,
+               length = elements.length;
+
+       for ( ; index < length; index++ ) {
+               elem = elements[ index ];
+               if ( !elem.style ) {
+                       continue;
+               }
+               values[ index ] = jQuery._data( elem, "olddisplay" );
+               if ( show ) {
+                       // Reset the inline display of this element to learn if it is
+                       // being hidden by cascaded rules or not
+                       if ( !values[ index ] && elem.style.display === "none" ) {
+                               elem.style.display = "";
+                       }
+
+                       // Set elements which have been overridden with display: none
+                       // in a stylesheet to whatever the default browser style is
+                       // for such an element
+                       if ( elem.style.display === "" && isHidden( elem ) ) {
+                               values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) );
+                       }
+               } else {
+                       display = curCSS( elem, "display" );
+
+                       if ( !values[ index ] && display !== "none" ) {
+                               jQuery._data( elem, "olddisplay", display );
+                       }
+               }
+       }
+
+       // Set the display of most of the elements in a second loop
+       // to avoid the constant reflow
+       for ( index = 0; index < length; index++ ) {
+               elem = elements[ index ];
+               if ( !elem.style ) {
+                       continue;
+               }
+               if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
+                       elem.style.display = show ? values[ index ] || "" : "none";
+               }
+       }
+
+       return elements;
+}
+
+jQuery.fn.extend({
+       css: function( name, value ) {
+               return jQuery.access( this, function( elem, name, value ) {
+                       return value !== undefined ?
+                               jQuery.style( elem, name, value ) :
+                               jQuery.css( elem, name );
+               }, name, value, arguments.length > 1 );
+       },
+       show: function() {
+               return showHide( this, true );
+       },
+       hide: function() {
+               return showHide( this );
+       },
+       toggle: function( state, fn2 ) {
+               var bool = typeof state === "boolean";
+
+               if ( jQuery.isFunction( state ) && jQuery.isFunction( fn2 ) ) {
+                       return eventsToggle.apply( this, arguments );
+               }
+
+               return this.each(function() {
+                       if ( bool ? state : isHidden( this ) ) {
+                               jQuery( this ).show();
+                       } else {
+                               jQuery( this ).hide();
+                       }
+               });
+       }
+});
+
+jQuery.extend({
+       // Add in style property hooks for overriding the default
+       // behavior of getting and setting a style property
+       cssHooks: {
+               opacity: {
+                       get: function( elem, computed ) {
+                               if ( computed ) {
+                                       // We should always get a number back from opacity
+                                       var ret = curCSS( elem, "opacity" );
+                                       return ret === "" ? "1" : ret;
+
+                               }
+                       }
+               }
+       },
+
+       // Exclude the following css properties to add px
+       cssNumber: {
+               "fillOpacity": true,
+               "fontWeight": true,
+               "lineHeight": true,
+               "opacity": true,
+               "orphans": true,
+               "widows": true,
+               "zIndex": true,
+               "zoom": true
+       },
+
+       // Add in properties whose names you wish to fix before
+       // setting or getting the value
+       cssProps: {
+               // normalize float css property
+               "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
+       },
+
+       // Get and set the style property on a DOM Node
+       style: function( elem, name, value, extra ) {
+               // Don't set styles on text and comment nodes
+               if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+                       return;
+               }
+
+               // Make sure that we're working with the right name
+               var ret, type, hooks,
+                       origName = jQuery.camelCase( name ),
+                       style = elem.style;
+
+               name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
+
+               // gets hook for the prefixed version
+               // followed by the unprefixed version
+               hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+               // Check if we're setting a value
+               if ( value !== undefined ) {
+                       type = typeof value;
+
+                       // convert relative number strings (+= or -=) to relative numbers. #7345
+                       if ( type === "string" && (ret = rrelNum.exec( value )) ) {
+                               value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
+                               // Fixes bug #9237
+                               type = "number";
+                       }
+
+                       // Make sure that NaN and null values aren't set. See: #7116
+                       if ( value == null || type === "number" && isNaN( value ) ) {
+                               return;
+                       }
+
+                       // If a number was passed in, add 'px' to the (except for certain CSS properties)
+                       if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
+                               value += "px";
+                       }
+
+                       // If a hook was provided, use that value, otherwise just set the specified value
+                       if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
+                               // Wrapped to prevent IE from throwing errors when 'invalid' values are provided
+                               // Fixes bug #5509
+                               try {
+                                       style[ name ] = value;
+                               } catch(e) {}
+                       }
+
+               } else {
+                       // If a hook was provided get the non-computed value from there
+                       if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
+                               return ret;
+                       }
+
+                       // Otherwise just get the value from the style object
+                       return style[ name ];
+               }
+       },
+
+       css: function( elem, name, numeric, extra ) {
+               var val, num, hooks,
+                       origName = jQuery.camelCase( name );
+
+               // Make sure that we're working with the right name
+               name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );
+
+               // gets hook for the prefixed version
+               // followed by the unprefixed version
+               hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+               // If a hook was provided get the computed value from there
+               if ( hooks && "get" in hooks ) {
+                       val = hooks.get( elem, true, extra );
+               }
+
+               // Otherwise, if a way to get the computed value exists, use that
+               if ( val === undefined ) {
+                       val = curCSS( elem, name );
+               }
+
+               //convert "normal" to computed value
+               if ( val === "normal" && name in cssNormalTransform ) {
+                       val = cssNormalTransform[ name ];
+               }
+
+               // Return, converting to number if forced or a qualifier was provided and val looks numeric
+               if ( numeric || extra !== undefined ) {
+                       num = parseFloat( val );
+                       return numeric || jQuery.isNumeric( num ) ? num || 0 : val;
+               }
+               return val;
+       },
+
+       // A method for quickly swapping in/out CSS properties to get correct calculations
+       swap: function( elem, options, callback ) {
+               var ret, name,
+                       old = {};
+
+               // Remember the old values, and insert the new ones
+               for ( name in options ) {
+                       old[ name ] = elem.style[ name ];
+                       elem.style[ name ] = options[ name ];
+               }
+
+               ret = callback.call( elem );
+
+               // Revert the old values
+               for ( name in options ) {
+                       elem.style[ name ] = old[ name ];
+               }
+
+               return ret;
+       }
+});
+
+// NOTE: To any future maintainer, we've window.getComputedStyle
+// because jsdom on node.js will break without it.
+if ( window.getComputedStyle ) {
+       curCSS = function( elem, name ) {
+               var ret, width, minWidth, maxWidth,
+                       computed = window.getComputedStyle( elem, null ),
+                       style = elem.style;
+
+               if ( computed ) {
+
+                       // getPropertyValue is only needed for .css('filter') in IE9, see #12537
+                       ret = computed.getPropertyValue( name ) || computed[ name ];
+
+                       if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
+                               ret = jQuery.style( elem, name );
+                       }
+
+                       // A tribute to the "awesome hack by Dean Edwards"
+                       // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right
+                       // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
+                       // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
+                       if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
+                               width = style.width;
+                               minWidth = style.minWidth;
+                               maxWidth = style.maxWidth;
+
+                               style.minWidth = style.maxWidth = style.width = ret;
+                               ret = computed.width;
+
+                               style.width = width;
+                               style.minWidth = minWidth;
+                               style.maxWidth = maxWidth;
+                       }
+               }
+
+               return ret;
+       };
+} else if ( document.documentElement.currentStyle ) {
+       curCSS = function( elem, name ) {
+               var left, rsLeft,
+                       ret = elem.currentStyle && elem.currentStyle[ name ],
+                       style = elem.style;
+
+               // Avoid setting ret to empty string here
+               // so we don't default to auto
+               if ( ret == null && style && style[ name ] ) {
+                       ret = style[ name ];
+               }
+
+               // From the awesome hack by Dean Edwards
+               // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+               // If we're not dealing with a regular pixel number
+               // but a number that has a weird ending, we need to convert it to pixels
+               // but not position css attributes, as those are proportional to the parent element instead
+               // and we can't measure the parent instead because it might trigger a "stacking dolls" problem
+               if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {
+
+                       // Remember the original values
+                       left = style.left;
+                       rsLeft = elem.runtimeStyle && elem.runtimeStyle.left;
+
+                       // Put in the new values to get a computed value out
+                       if ( rsLeft ) {
+                               elem.runtimeStyle.left = elem.currentStyle.left;
+                       }
+                       style.left = name === "fontSize" ? "1em" : ret;
+                       ret = style.pixelLeft + "px";
+
+                       // Revert the changed values
+                       style.left = left;
+                       if ( rsLeft ) {
+                               elem.runtimeStyle.left = rsLeft;
+                       }
+               }
+
+               return ret === "" ? "auto" : ret;
+       };
+}
+
+function setPositiveNumber( elem, value, subtract ) {
+       var matches = rnumsplit.exec( value );
+       return matches ?
+                       Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
+                       value;
+}
+
+function augmentWidthOrHeight( elem, name, extra, isBorderBox ) {
+       var i = extra === ( isBorderBox ? "border" : "content" ) ?
+               // If we already have the right measurement, avoid augmentation
+               4 :
+               // Otherwise initialize for horizontal or vertical properties
+               name === "width" ? 1 : 0,
+
+               val = 0;
+
+       for ( ; i < 4; i += 2 ) {
+               // both box models exclude margin, so add it if we want it
+               if ( extra === "margin" ) {
+                       // we use jQuery.css instead of curCSS here
+                       // because of the reliableMarginRight CSS hook!
+                       val += jQuery.css( elem, extra + cssExpand[ i ], true );
+               }
+
+               // From this point on we use curCSS for maximum performance (relevant in animations)
+               if ( isBorderBox ) {
+                       // border-box includes padding, so remove it if we want content
+                       if ( extra === "content" ) {
+                               val -= parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0;
+                       }
+
+                       // at this point, extra isn't border nor margin, so remove border
+                       if ( extra !== "margin" ) {
+                               val -= parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0;
+                       }
+               } else {
+                       // at this point, extra isn't content, so add padding
+                       val += parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0;
+
+                       // at this point, extra isn't content nor padding, so add border
+                       if ( extra !== "padding" ) {
+                               val += parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0;
+                       }
+               }
+       }
+
+       return val;
+}
+
+function getWidthOrHeight( elem, name, extra ) {
+
+       // Start with offset property, which is equivalent to the border-box value
+       var val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+               valueIsBorderBox = true,
+               isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box";
+
+       // some non-html elements return undefined for offsetWidth, so check for null/undefined
+       // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
+       // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
+       if ( val <= 0 || val == null ) {
+               // Fall back to computed then uncomputed css if necessary
+               val = curCSS( elem, name );
+               if ( val < 0 || val == null ) {
+                       val = elem.style[ name ];
+               }
+
+               // Computed unit is not pixels. Stop here and return.
+               if ( rnumnonpx.test(val) ) {
+                       return val;
+               }
+
+               // we need the check for style in case a browser which returns unreliable values
+               // for getComputedStyle silently falls back to the reliable elem.style
+               valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] );
+
+               // Normalize "", auto, and prepare for extra
+               val = parseFloat( val ) || 0;
+       }
+
+       // use the active box-sizing model to add/subtract irrelevant styles
+       return ( val +
+               augmentWidthOrHeight(
+                       elem,
+                       name,
+                       extra || ( isBorderBox ? "border" : "content" ),
+                       valueIsBorderBox
+               )
+       ) + "px";
+}
+
+
+// Try to determine the default display value of an element
+function css_defaultDisplay( nodeName ) {
+       if ( elemdisplay[ nodeName ] ) {
+               return elemdisplay[ nodeName ];
+       }
+
+       var elem = jQuery( "<" + nodeName + ">" ).appendTo( document.body ),
+               display = elem.css("display");
+       elem.remove();
+
+       // If the simple way fails,
+       // get element's real default display by attaching it to a temp iframe
+       if ( display === "none" || display === "" ) {
+               // Use the already-created iframe if possible
+               iframe = document.body.appendChild(
+                       iframe || jQuery.extend( document.createElement("iframe"), {
+                               frameBorder: 0,
+                               width: 0,
+                               height: 0
+                       })
+               );
+
+               // Create a cacheable copy of the iframe document on first call.
+               // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML
+               // document to it; WebKit & Firefox won't allow reusing the iframe document.
+               if ( !iframeDoc || !iframe.createElement ) {
+                       iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document;
+                       iframeDoc.write("<!doctype html><html><body>");
+                       iframeDoc.close();
+               }
+
+               elem = iframeDoc.body.appendChild( iframeDoc.createElement(nodeName) );
+
+               display = curCSS( elem, "display" );
+               document.body.removeChild( iframe );
+       }
+
+       // Store the correct default display
+       elemdisplay[ nodeName ] = display;
+
+       return display;
+}
+
+jQuery.each([ "height", "width" ], function( i, name ) {
+       jQuery.cssHooks[ name ] = {
+               get: function( elem, computed, extra ) {
+                       if ( computed ) {
+                               // certain elements can have dimension info if we invisibly show them
+                               // however, it must have a current display style that would benefit from this
+                               if ( elem.offsetWidth === 0 && rdisplayswap.test( curCSS( elem, "display" ) ) ) {
+                                       return jQuery.swap( elem, cssShow, function() {
+                                               return getWidthOrHeight( elem, name, extra );
+                                       });
+                               } else {
+                                       return getWidthOrHeight( elem, name, extra );
+                               }
+                       }
+               },
+
+               set: function( elem, value, extra ) {
+                       return setPositiveNumber( elem, value, extra ?
+                               augmentWidthOrHeight(
+                                       elem,
+                                       name,
+                                       extra,
+                                       jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box"
+                               ) : 0
+                       );
+               }
+       };
+});
+
+if ( !jQuery.support.opacity ) {
+       jQuery.cssHooks.opacity = {
+               get: function( elem, computed ) {
+                       // IE uses filters for opacity
+                       return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
+                               ( 0.01 * parseFloat( RegExp.$1 ) ) + "" :
+                               computed ? "1" : "";
+               },
+
+               set: function( elem, value ) {
+                       var style = elem.style,
+                               currentStyle = elem.currentStyle,
+                               opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
+                               filter = currentStyle && currentStyle.filter || style.filter || "";
+
+                       // IE has trouble with opacity if it does not have layout
+                       // Force it by setting the zoom level
+                       style.zoom = 1;
+
+                       // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
+                       if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" &&
+                               style.removeAttribute ) {
+
+                               // Setting style.filter to null, "" & " " still leave "filter:" in the cssText
+                               // if "filter:" is present at all, clearType is disabled, we want to avoid this
+                               // style.removeAttribute is IE Only, but so apparently is this code path...
+                               style.removeAttribute( "filter" );
+
+                               // if there there is no filter style applied in a css rule, we are done
+                               if ( currentStyle && !currentStyle.filter ) {
+                                       return;
+                               }
+                       }
+
+                       // otherwise, set new filter values
+                       style.filter = ralpha.test( filter ) ?
+                               filter.replace( ralpha, opacity ) :
+                               filter + " " + opacity;
+               }
+       };
+}
+
+// These hooks cannot be added until DOM ready because the support test
+// for it is not run until after DOM ready
+jQuery(function() {
+       if ( !jQuery.support.reliableMarginRight ) {
+               jQuery.cssHooks.marginRight = {
+                       get: function( elem, computed ) {
+                               // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+                               // Work around by temporarily setting element display to inline-block
+                               return jQuery.swap( elem, { "display": "inline-block" }, function() {
+                                       if ( computed ) {
+                                               return curCSS( elem, "marginRight" );
+                                       }
+                               });
+                       }
+               };
+       }
+
+       // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+       // getComputedStyle returns percent when specified for top/left/bottom/right
+       // rather than make the css module depend on the offset module, we just check for it here
+       if ( !jQuery.support.pixelPosition && jQuery.fn.position ) {
+               jQuery.each( [ "top", "left" ], function( i, prop ) {
+                       jQuery.cssHooks[ prop ] = {
+                               get: function( elem, computed ) {
+                                       if ( computed ) {
+                                               var ret = curCSS( elem, prop );
+                                               // if curCSS returns percentage, fallback to offset
+                                               return rnumnonpx.test( ret ) ? jQuery( elem ).position()[ prop ] + "px" : ret;
+                                       }
+                               }
+                       };
+               });
+       }
+
+});
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+       jQuery.expr.filters.hidden = function( elem ) {
+               return ( elem.offsetWidth === 0 && elem.offsetHeight === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || curCSS( elem, "display" )) === "none");
+       };
+
+       jQuery.expr.filters.visible = function( elem ) {
+               return !jQuery.expr.filters.hidden( elem );
+       };
+}
+
+// These hooks are used by animate to expand properties
+jQuery.each({
+       margin: "",
+       padding: "",
+       border: "Width"
+}, function( prefix, suffix ) {
+       jQuery.cssHooks[ prefix + suffix ] = {
+               expand: function( value ) {
+                       var i,
+
+                               // assumes a single number if not a string
+                               parts = typeof value === "string" ? value.split(" ") : [ value ],
+                               expanded = {};
+
+                       for ( i = 0; i < 4; i++ ) {
+                               expanded[ prefix + cssExpand[ i ] + suffix ] =
+                                       parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+                       }
+
+                       return expanded;
+               }
+       };
+
+       if ( !rmargin.test( prefix ) ) {
+               jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+       }
+});
+var r20 = /%20/g,
+       rbracket = /\[\]$/,
+       rCRLF = /\r?\n/g,
+       rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,
+       rselectTextarea = /^(?:select|textarea)/i;
+
+jQuery.fn.extend({
+       serialize: function() {
+               return jQuery.param( this.serializeArray() );
+       },
+       serializeArray: function() {
+               return this.map(function(){
+                       return this.elements ? jQuery.makeArray( this.elements ) : this;
+               })
+               .filter(function(){
+                       return this.name && !this.disabled &&
+                               ( this.checked || rselectTextarea.test( this.nodeName ) ||
+                                       rinput.test( this.type ) );
+               })
+               .map(function( i, elem ){
+                       var val = jQuery( this ).val();
+
+                       return val == null ?
+                               null :
+                               jQuery.isArray( val ) ?
+                                       jQuery.map( val, function( val, i ){
+                                               return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+                                       }) :
+                                       { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+               }).get();
+       }
+});
+
+//Serialize an array of form elements or a set of
+//key/values into a query string
+jQuery.param = function( a, traditional ) {
+       var prefix,
+               s = [],
+               add = function( key, value ) {
+                       // If value is a function, invoke it and return its value
+                       value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
+                       s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+               };
+
+       // Set traditional to true for jQuery <= 1.3.2 behavior.
+       if ( traditional === undefined ) {
+               traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
+       }
+
+       // If an array was passed in, assume that it is an array of form elements.
+       if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+               // Serialize the form elements
+               jQuery.each( a, function() {
+                       add( this.name, this.value );
+               });
+
+       } else {
+               // If traditional, encode the "old" way (the way 1.3.2 or older
+               // did it), otherwise encode params recursively.
+               for ( prefix in a ) {
+                       buildParams( prefix, a[ prefix ], traditional, add );
+               }
+       }
+
+       // Return the resulting serialization
+       return s.join( "&" ).replace( r20, "+" );
+};
+
+function buildParams( prefix, obj, traditional, add ) {
+       var name;
+
+       if ( jQuery.isArray( obj ) ) {
+               // Serialize array item.
+               jQuery.each( obj, function( i, v ) {
+                       if ( traditional || rbracket.test( prefix ) ) {
+                               // Treat each array item as a scalar.
+                               add( prefix, v );
+
+                       } else {
+                               // If array item is non-scalar (array or object), encode its
+                               // numeric index to resolve deserialization ambiguity issues.
+                               // Note that rack (as of 1.0.0) can't currently deserialize
+                               // nested arrays properly, and attempting to do so may cause
+                               // a server error. Possible fixes are to modify rack's
+                               // deserialization algorithm or to provide an option or flag
+                               // to force array serialization to be shallow.
+                               buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add );
+                       }
+               });
+
+       } else if ( !traditional && jQuery.type( obj ) === "object" ) {
+               // Serialize object item.
+               for ( name in obj ) {
+                       buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+               }
+
+       } else {
+               // Serialize scalar item.
+               add( prefix, obj );
+       }
+}
+var
+       // Document location
+       ajaxLocParts,
+       ajaxLocation,
+
+       rhash = /#.*$/,
+       rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
+       // #7653, #8125, #8152: local protocol detection
+       rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,
+       rnoContent = /^(?:GET|HEAD)$/,
+       rprotocol = /^\/\//,
+       rquery = /\?/,
+       rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
+       rts = /([?&])_=[^&]*/,
+       rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,
+
+       // Keep a copy of the old load method
+       _load = jQuery.fn.load,
+
+       /* Prefilters
+        * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+        * 2) These are called:
+        *    - BEFORE asking for a transport
+        *    - AFTER param serialization (s.data is a string if s.processData is true)
+        * 3) key is the dataType
+        * 4) the catchall symbol "*" can be used
+        * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+        */
+       prefilters = {},
+
+       /* Transports bindings
+        * 1) key is the dataType
+        * 2) the catchall symbol "*" can be used
+        * 3) selection will start with transport dataType and THEN go to "*" if needed
+        */
+       transports = {},
+
+       // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+       allTypes = ["*/"] + ["*"];
+
+// #8138, IE may throw an exception when accessing
+// a field from window.location if document.domain has been set
+try {
+       ajaxLocation = location.href;
+} catch( e ) {
+       // Use the href attribute of an A element
+       // since IE will modify it given document.location
+       ajaxLocation = document.createElement( "a" );
+       ajaxLocation.href = "";
+       ajaxLocation = ajaxLocation.href;
+}
+
+// Segment location into parts
+ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+       // dataTypeExpression is optional and defaults to "*"
+       return function( dataTypeExpression, func ) {
+
+               if ( typeof dataTypeExpression !== "string" ) {
+                       func = dataTypeExpression;
+                       dataTypeExpression = "*";
+               }
+
+               var dataType, list, placeBefore,
+                       dataTypes = dataTypeExpression.toLowerCase().split( core_rspace ),
+                       i = 0,
+                       length = dataTypes.length;
+
+               if ( jQuery.isFunction( func ) ) {
+                       // For each dataType in the dataTypeExpression
+                       for ( ; i < length; i++ ) {
+                               dataType = dataTypes[ i ];
+                               // We control if we're asked to add before
+                               // any existing element
+                               placeBefore = /^\+/.test( dataType );
+                               if ( placeBefore ) {
+                                       dataType = dataType.substr( 1 ) || "*";
+                               }
+                               list = structure[ dataType ] = structure[ dataType ] || [];
+                               // then we add to the structure accordingly
+                               list[ placeBefore ? "unshift" : "push" ]( func );
+                       }
+               }
+       };
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,
+               dataType /* internal */, inspected /* internal */ ) {
+
+       dataType = dataType || options.dataTypes[ 0 ];
+       inspected = inspected || {};
+
+       inspected[ dataType ] = true;
+
+       var selection,
+               list = structure[ dataType ],
+               i = 0,
+               length = list ? list.length : 0,
+               executeOnly = ( structure === prefilters );
+
+       for ( ; i < length && ( executeOnly || !selection ); i++ ) {
+               selection = list[ i ]( options, originalOptions, jqXHR );
+               // If we got redirected to another dataType
+               // we try there if executing only and not done already
+               if ( typeof selection === "string" ) {
+                       if ( !executeOnly || inspected[ selection ] ) {
+                               selection = undefined;
+                       } else {
+                               options.dataTypes.unshift( selection );
+                               selection = inspectPrefiltersOrTransports(
+                                               structure, options, originalOptions, jqXHR, selection, inspected );
+                       }
+               }
+       }
+       // If we're only executing or nothing was selected
+       // we try the catchall dataType if not done already
+       if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) {
+               selection = inspectPrefiltersOrTransports(
+                               structure, options, originalOptions, jqXHR, "*", inspected );
+       }
+       // unnecessary when only executing (prefilters)
+       // but it'll be ignored by the caller in that case
+       return selection;
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+       var key, deep,
+               flatOptions = jQuery.ajaxSettings.flatOptions || {};
+       for ( key in src ) {
+               if ( src[ key ] !== undefined ) {
+                       ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
+               }
+       }
+       if ( deep ) {
+               jQuery.extend( true, target, deep );
+       }
+}
+
+jQuery.fn.load = function( url, params, callback ) {
+       if ( typeof url !== "string" && _load ) {
+               return _load.apply( this, arguments );
+       }
+
+       // Don't do a request if no elements are being requested
+       if ( !this.length ) {
+               return this;
+       }
+
+       var selector, type, response,
+               self = this,
+               off = url.indexOf(" ");
+
+       if ( off >= 0 ) {
+               selector = url.slice( off, url.length );
+               url = url.slice( 0, off );
+       }
+
+       // If it's a function
+       if ( jQuery.isFunction( params ) ) {
+
+               // We assume that it's the callback
+               callback = params;
+               params = undefined;
+
+       // Otherwise, build a param string
+       } else if ( params && typeof params === "object" ) {
+               type = "POST";
+       }
+
+       // Request the remote document
+       jQuery.ajax({
+               url: url,
+
+               // if "type" variable is undefined, then "GET" method will be used
+               type: type,
+               dataType: "html",
+               data: params,
+               complete: function( jqXHR, status ) {
+                       if ( callback ) {
+                               self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );
+                       }
+               }
+       }).done(function( responseText ) {
+
+               // Save response for use in complete callback
+               response = arguments;
+
+               // See if a selector was specified
+               self.html( selector ?
+
+                       // Create a dummy div to hold the results
+                       jQuery("<div>")
+
+                               // inject the contents of the document in, removing the scripts
+                               // to avoid any 'Permission Denied' errors in IE
+                               .append( responseText.replace( rscript, "" ) )
+
+                               // Locate the specified elements
+                               .find( selector ) :
+
+                       // If not, just inject the full result
+                       responseText );
+
+       });
+
+       return this;
+};
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){
+       jQuery.fn[ o ] = function( f ){
+               return this.on( o, f );
+       };
+});
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+       jQuery[ method ] = function( url, data, callback, type ) {
+               // shift arguments if data argument was omitted
+               if ( jQuery.isFunction( data ) ) {
+                       type = type || callback;
+                       callback = data;
+                       data = undefined;
+               }
+
+               return jQuery.ajax({
+                       type: method,
+                       url: url,
+                       data: data,
+                       success: callback,
+                       dataType: type
+               });
+       };
+});
+
+jQuery.extend({
+
+       getScript: function( url, callback ) {
+               return jQuery.get( url, undefined, callback, "script" );
+       },
+
+       getJSON: function( url, data, callback ) {
+               return jQuery.get( url, data, callback, "json" );
+       },
+
+       // Creates a full fledged settings object into target
+       // with both ajaxSettings and settings fields.
+       // If target is omitted, writes into ajaxSettings.
+       ajaxSetup: function( target, settings ) {
+               if ( settings ) {
+                       // Building a settings object
+                       ajaxExtend( target, jQuery.ajaxSettings );
+               } else {
+                       // Extending ajaxSettings
+                       settings = target;
+                       target = jQuery.ajaxSettings;
+               }
+               ajaxExtend( target, settings );
+               return target;
+       },
+
+       ajaxSettings: {
+               url: ajaxLocation,
+               isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
+               global: true,
+               type: "GET",
+               contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+               processData: true,
+               async: true,
+               /*
+               timeout: 0,
+               data: null,
+               dataType: null,
+               username: null,
+               password: null,
+               cache: null,
+               throws: false,
+               traditional: false,
+               headers: {},
+               */
+
+               accepts: {
+                       xml: "application/xml, text/xml",
+                       html: "text/html",
+                       text: "text/plain",
+                       json: "application/json, text/javascript",
+                       "*": allTypes
+               },
+
+               contents: {
+                       xml: /xml/,
+                       html: /html/,
+                       json: /json/
+               },
+
+               responseFields: {
+                       xml: "responseXML",
+                       text: "responseText"
+               },
+
+               // List of data converters
+               // 1) key format is "source_type destination_type" (a single space in-between)
+               // 2) the catchall symbol "*" can be used for source_type
+               converters: {
+
+                       // Convert anything to text
+                       "* text": window.String,
+
+                       // Text to html (true = no transformation)
+                       "text html": true,
+
+                       // Evaluate text as a json expression
+                       "text json": jQuery.parseJSON,
+
+                       // Parse text as xml
+                       "text xml": jQuery.parseXML
+               },
+
+               // For options that shouldn't be deep extended:
+               // you can add your own custom options here if
+               // and when you create one that shouldn't be
+               // deep extended (see ajaxExtend)
+               flatOptions: {
+                       context: true,
+                       url: true
+               }
+       },
+
+       ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+       ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+       // Main method
+       ajax: function( url, options ) {
+
+               // If url is an object, simulate pre-1.5 signature
+               if ( typeof url === "object" ) {
+                       options = url;
+                       url = undefined;
+               }
+
+               // Force options to be an object
+               options = options || {};
+
+               var // ifModified key
+                       ifModifiedKey,
+                       // Response headers
+                       responseHeadersString,
+                       responseHeaders,
+                       // transport
+                       transport,
+                       // timeout handle
+                       timeoutTimer,
+                       // Cross-domain detection vars
+                       parts,
+                       // To know if global events are to be dispatched
+                       fireGlobals,
+                       // Loop variable
+                       i,
+                       // Create the final options object
+                       s = jQuery.ajaxSetup( {}, options ),
+                       // Callbacks context
+                       callbackContext = s.context || s,
+                       // Context for global events
+                       // It's the callbackContext if one was provided in the options
+                       // and if it's a DOM node or a jQuery collection
+                       globalEventContext = callbackContext !== s &&
+                               ( callbackContext.nodeType || callbackContext instanceof jQuery ) ?
+                                               jQuery( callbackContext ) : jQuery.event,
+                       // Deferreds
+                       deferred = jQuery.Deferred(),
+                       completeDeferred = jQuery.Callbacks( "once memory" ),
+                       // Status-dependent callbacks
+                       statusCode = s.statusCode || {},
+                       // Headers (they are sent all at once)
+                       requestHeaders = {},
+                       requestHeadersNames = {},
+                       // The jqXHR state
+                       state = 0,
+                       // Default abort message
+                       strAbort = "canceled",
+                       // Fake xhr
+                       jqXHR = {
+
+                               readyState: 0,
+
+                               // Caches the header
+                               setRequestHeader: function( name, value ) {
+                                       if ( !state ) {
+                                               var lname = name.toLowerCase();
+                                               name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+                                               requestHeaders[ name ] = value;
+                                       }
+                                       return this;
+                               },
+
+                               // Raw string
+                               getAllResponseHeaders: function() {
+                                       return state === 2 ? responseHeadersString : null;
+                               },
+
+                               // Builds headers hashtable if needed
+                               getResponseHeader: function( key ) {
+                                       var match;
+                                       if ( state === 2 ) {
+                                               if ( !responseHeaders ) {
+                                                       responseHeaders = {};
+                                                       while( ( match = rheaders.exec( responseHeadersString ) ) ) {
+                                                               responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+                                                       }
+                                               }
+                                               match = responseHeaders[ key.toLowerCase() ];
+                                       }
+                                       return match === undefined ? null : match;
+                               },
+
+                               // Overrides response content-type header
+                               overrideMimeType: function( type ) {
+                                       if ( !state ) {
+                                               s.mimeType = type;
+                                       }
+                                       return this;
+                               },
+
+                               // Cancel the request
+                               abort: function( statusText ) {
+                                       statusText = statusText || strAbort;
+                                       if ( transport ) {
+                                               transport.abort( statusText );
+                                       }
+                                       done( 0, statusText );
+                                       return this;
+                               }
+                       };
+
+               // Callback for when everything is done
+               // It is defined here because jslint complains if it is declared
+               // at the end of the function (which would be more logical and readable)
+               function done( status, nativeStatusText, responses, headers ) {
+                       var isSuccess, success, error, response, modified,
+                               statusText = nativeStatusText;
+
+                       // Called once
+                       if ( state === 2 ) {
+                               return;
+                       }
+
+                       // State is "done" now
+                       state = 2;
+
+                       // Clear timeout if it exists
+                       if ( timeoutTimer ) {
+                               clearTimeout( timeoutTimer );
+                       }
+
+                       // Dereference transport for early garbage collection
+                       // (no matter how long the jqXHR object will be used)
+                       transport = undefined;
+
+                       // Cache response headers
+                       responseHeadersString = headers || "";
+
+                       // Set readyState
+                       jqXHR.readyState = status > 0 ? 4 : 0;
+
+                       // Get response data
+                       if ( responses ) {
+                               response = ajaxHandleResponses( s, jqXHR, responses );
+                       }
+
+                       // If successful, handle type chaining
+                       if ( status >= 200 && status < 300 || status === 304 ) {
+
+                               // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+                               if ( s.ifModified ) {
+
+                                       modified = jqXHR.getResponseHeader("Last-Modified");
+                                       if ( modified ) {
+                                               jQuery.lastModified[ ifModifiedKey ] = modified;
+                                       }
+                                       modified = jqXHR.getResponseHeader("Etag");
+                                       if ( modified ) {
+                                               jQuery.etag[ ifModifiedKey ] = modified;
+                                       }
+                               }
+
+                               // If not modified
+                               if ( status === 304 ) {
+
+                                       statusText = "notmodified";
+                                       isSuccess = true;
+
+                               // If we have data
+                               } else {
+
+                                       isSuccess = ajaxConvert( s, response );
+                                       statusText = isSuccess.state;
+                                       success = isSuccess.data;
+                                       error = isSuccess.error;
+                                       isSuccess = !error;
+                               }
+                       } else {
+                               // We extract error from statusText
+                               // then normalize statusText and status for non-aborts
+                               error = statusText;
+                               if ( !statusText || status ) {
+                                       statusText = "error";
+                                       if ( status < 0 ) {
+                                               status = 0;
+                                       }
+                               }
+                       }
+
+                       // Set data for the fake xhr object
+                       jqXHR.status = status;
+                       jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+                       // Success/Error
+                       if ( isSuccess ) {
+                               deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+                       } else {
+                               deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+                       }
+
+                       // Status-dependent callbacks
+                       jqXHR.statusCode( statusCode );
+                       statusCode = undefined;
+
+                       if ( fireGlobals ) {
+                               globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ),
+                                               [ jqXHR, s, isSuccess ? success : error ] );
+                       }
+
+                       // Complete
+                       completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+                       if ( fireGlobals ) {
+                               globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+                               // Handle the global AJAX counter
+                               if ( !( --jQuery.active ) ) {
+                                       jQuery.event.trigger( "ajaxStop" );
+                               }
+                       }
+               }
+
+               // Attach deferreds
+               deferred.promise( jqXHR );
+               jqXHR.success = jqXHR.done;
+               jqXHR.error = jqXHR.fail;
+               jqXHR.complete = completeDeferred.add;
+
+               // Status-dependent callbacks
+               jqXHR.statusCode = function( map ) {
+                       if ( map ) {
+                               var tmp;
+                               if ( state < 2 ) {
+                                       for ( tmp in map ) {
+                                               statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ];
+                                       }
+                               } else {
+                                       tmp = map[ jqXHR.status ];
+                                       jqXHR.always( tmp );
+                               }
+                       }
+                       return this;
+               };
+
+               // Remove hash character (#7531: and string promotion)
+               // Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+               // We also use the url parameter if available
+               s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+               // Extract dataTypes list
+               s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( core_rspace );
+
+               // A cross-domain request is in order when we have a protocol:host:port mismatch
+               if ( s.crossDomain == null ) {
+                       parts = rurl.exec( s.url.toLowerCase() );
+                       s.crossDomain = !!( parts &&
+                               ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
+                                       ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) !=
+                                               ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) )
+                       );
+               }
+
+               // Convert data if not already a string
+               if ( s.data && s.processData && typeof s.data !== "string" ) {
+                       s.data = jQuery.param( s.data, s.traditional );
+               }
+
+               // Apply prefilters
+               inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+               // If request was aborted inside a prefilter, stop there
+               if ( state === 2 ) {
+                       return jqXHR;
+               }
+
+               // We can fire global events as of now if asked to
+               fireGlobals = s.global;
+
+               // Uppercase the type
+               s.type = s.type.toUpperCase();
+
+               // Determine if request has content
+               s.hasContent = !rnoContent.test( s.type );
+
+               // Watch for a new set of requests
+               if ( fireGlobals && jQuery.active++ === 0 ) {
+                       jQuery.event.trigger( "ajaxStart" );
+               }
+
+               // More options handling for requests with no content
+               if ( !s.hasContent ) {
+
+                       // If data is available, append data to url
+                       if ( s.data ) {
+                               s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data;
+                               // #9682: remove data so that it's not used in an eventual retry
+                               delete s.data;
+                       }
+
+                       // Get ifModifiedKey before adding the anti-cache parameter
+                       ifModifiedKey = s.url;
+
+                       // Add anti-cache in url if needed
+                       if ( s.cache === false ) {
+
+                               var ts = jQuery.now(),
+                                       // try replacing _= if it is there
+                                       ret = s.url.replace( rts, "$1_=" + ts );
+
+                               // if nothing was replaced, add timestamp to the end
+                               s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" );
+                       }
+               }
+
+               // Set the correct header, if data is being sent
+               if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+                       jqXHR.setRequestHeader( "Content-Type", s.contentType );
+               }
+
+               // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+               if ( s.ifModified ) {
+                       ifModifiedKey = ifModifiedKey || s.url;
+                       if ( jQuery.lastModified[ ifModifiedKey ] ) {
+                               jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] );
+                       }
+                       if ( jQuery.etag[ ifModifiedKey ] ) {
+                               jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] );
+                       }
+               }
+
+               // Set the Accepts header for the server, depending on the dataType
+               jqXHR.setRequestHeader(
+                       "Accept",
+                       s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+                               s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+                               s.accepts[ "*" ]
+               );
+
+               // Check for headers option
+               for ( i in s.headers ) {
+                       jqXHR.setRequestHeader( i, s.headers[ i ] );
+               }
+
+               // Allow custom headers/mimetypes and early abort
+               if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+                               // Abort if not done already and return
+                               return jqXHR.abort();
+
+               }
+
+               // aborting is no longer a cancellation
+               strAbort = "abort";
+
+               // Install callbacks on deferreds
+               for ( i in { success: 1, error: 1, complete: 1 } ) {
+                       jqXHR[ i ]( s[ i ] );
+               }
+
+               // Get transport
+               transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+               // If no transport, we auto-abort
+               if ( !transport ) {
+                       done( -1, "No Transport" );
+               } else {
+                       jqXHR.readyState = 1;
+                       // Send global event
+                       if ( fireGlobals ) {
+                               globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+                       }
+                       // Timeout
+                       if ( s.async && s.timeout > 0 ) {
+                               timeoutTimer = setTimeout( function(){
+                                       jqXHR.abort( "timeout" );
+                               }, s.timeout );
+                       }
+
+                       try {
+                               state = 1;
+                               transport.send( requestHeaders, done );
+                       } catch (e) {
+                               // Propagate exception as error if not done
+                               if ( state < 2 ) {
+                                       done( -1, e );
+                               // Simply rethrow otherwise
+                               } else {
+                                       throw e;
+                               }
+                       }
+               }
+
+               return jqXHR;
+       },
+
+       // Counter for holding the number of active queries
+       active: 0,
+
+       // Last-Modified header cache for next request
+       lastModified: {},
+       etag: {}
+
+});
+
+/* Handles responses to an ajax request:
+ * - sets all responseXXX fields accordingly
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+       var ct, type, finalDataType, firstDataType,
+               contents = s.contents,
+               dataTypes = s.dataTypes,
+               responseFields = s.responseFields;
+
+       // Fill responseXXX fields
+       for ( type in responseFields ) {
+               if ( type in responses ) {
+                       jqXHR[ responseFields[type] ] = responses[ type ];
+               }
+       }
+
+       // Remove auto dataType and get content-type in the process
+       while( dataTypes[ 0 ] === "*" ) {
+               dataTypes.shift();
+               if ( ct === undefined ) {
+                       ct = s.mimeType || jqXHR.getResponseHeader( "content-type" );
+               }
+       }
+
+       // Check if we're dealing with a known content-type
+       if ( ct ) {
+               for ( type in contents ) {
+                       if ( contents[ type ] && contents[ type ].test( ct ) ) {
+                               dataTypes.unshift( type );
+                               break;
+                       }
+               }
+       }
+
+       // Check to see if we have a response for the expected dataType
+       if ( dataTypes[ 0 ] in responses ) {
+               finalDataType = dataTypes[ 0 ];
+       } else {
+               // Try convertible dataTypes
+               for ( type in responses ) {
+                       if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
+                               finalDataType = type;
+                               break;
+                       }
+                       if ( !firstDataType ) {
+                               firstDataType = type;
+                       }
+               }
+               // Or just use first one
+               finalDataType = finalDataType || firstDataType;
+       }
+
+       // If we found a dataType
+       // We add the dataType to the list if needed
+       // and return the corresponding response
+       if ( finalDataType ) {
+               if ( finalDataType !== dataTypes[ 0 ] ) {
+                       dataTypes.unshift( finalDataType );
+               }
+               return responses[ finalDataType ];
+       }
+}
+
+// Chain conversions given the request and the original response
+function ajaxConvert( s, response ) {
+
+       var conv, conv2, current, tmp,
+               // Work with a copy of dataTypes in case we need to modify it for conversion
+               dataTypes = s.dataTypes.slice(),
+               prev = dataTypes[ 0 ],
+               converters = {},
+               i = 0;
+
+       // Apply the dataFilter if provided
+       if ( s.dataFilter ) {
+               response = s.dataFilter( response, s.dataType );
+       }
+
+       // Create converters map with lowercased keys
+       if ( dataTypes[ 1 ] ) {
+               for ( conv in s.converters ) {
+                       converters[ conv.toLowerCase() ] = s.converters[ conv ];
+               }
+       }
+
+       // Convert to each sequential dataType, tolerating list modification
+       for ( ; (current = dataTypes[++i]); ) {
+
+               // There's only work to do if current dataType is non-auto
+               if ( current !== "*" ) {
+
+                       // Convert response if prev dataType is non-auto and differs from current
+                       if ( prev !== "*" && prev !== current ) {
+
+                               // Seek a direct converter
+                               conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+                               // If none found, seek a pair
+                               if ( !conv ) {
+                                       for ( conv2 in converters ) {
+
+                                               // If conv2 outputs current
+                                               tmp = conv2.split(" ");
+                                               if ( tmp[ 1 ] === current ) {
+
+                                                       // If prev can be converted to accepted input
+                                                       conv = converters[ prev + " " + tmp[ 0 ] ] ||
+                                                               converters[ "* " + tmp[ 0 ] ];
+                                                       if ( conv ) {
+                                                               // Condense equivalence converters
+                                                               if ( conv === true ) {
+                                                                       conv = converters[ conv2 ];
+
+                                                               // Otherwise, insert the intermediate dataType
+                                                               } else if ( converters[ conv2 ] !== true ) {
+                                                                       current = tmp[ 0 ];
+                                                                       dataTypes.splice( i--, 0, current );
+                                                               }
+
+                                                               break;
+                                                       }
+                                               }
+                                       }
+                               }
+
+                               // Apply converter (if not an equivalence)
+                               if ( conv !== true ) {
+
+                                       // Unless errors are allowed to bubble, catch and return them
+                                       if ( conv && s["throws"] ) {
+                                               response = conv( response );
+                                       } else {
+                                               try {
+                                                       response = conv( response );
+                                               } catch ( e ) {
+                                                       return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
+                                               }
+                                       }
+                               }
+                       }
+
+                       // Update prev for next iteration
+                       prev = current;
+               }
+       }
+
+       return { state: "success", data: response };
+}
+var oldCallbacks = [],
+       rquestion = /\?/,
+       rjsonp = /(=)\?(?=&|$)|\?\?/,
+       nonce = jQuery.now();
+
+// Default jsonp settings
+jQuery.ajaxSetup({
+       jsonp: "callback",
+       jsonpCallback: function() {
+               var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
+               this[ callback ] = true;
+               return callback;
+       }
+});
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+       var callbackName, overwritten, responseContainer,
+               data = s.data,
+               url = s.url,
+               hasCallback = s.jsonp !== false,
+               replaceInUrl = hasCallback && rjsonp.test( url ),
+               replaceInData = hasCallback && !replaceInUrl && typeof data === "string" &&
+                       !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") &&
+                       rjsonp.test( data );
+
+       // Handle iff the expected data type is "jsonp" or we have a parameter to set
+       if ( s.dataTypes[ 0 ] === "jsonp" || replaceInUrl || replaceInData ) {
+
+               // Get callback name, remembering preexisting value associated with it
+               callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+                       s.jsonpCallback() :
+                       s.jsonpCallback;
+               overwritten = window[ callbackName ];
+
+               // Insert callback into url or form data
+               if ( replaceInUrl ) {
+                       s.url = url.replace( rjsonp, "$1" + callbackName );
+               } else if ( replaceInData ) {
+                       s.data = data.replace( rjsonp, "$1" + callbackName );
+               } else if ( hasCallback ) {
+                       s.url += ( rquestion.test( url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+               }
+
+               // Use data converter to retrieve json after script execution
+               s.converters["script json"] = function() {
+                       if ( !responseContainer ) {
+                               jQuery.error( callbackName + " was not called" );
+                       }
+                       return responseContainer[ 0 ];
+               };
+
+               // force json dataType
+               s.dataTypes[ 0 ] = "json";
+
+               // Install callback
+               window[ callbackName ] = function() {
+                       responseContainer = arguments;
+               };
+
+               // Clean-up function (fires after converters)
+               jqXHR.always(function() {
+                       // Restore preexisting value
+                       window[ callbackName ] = overwritten;
+
+                       // Save back as free
+                       if ( s[ callbackName ] ) {
+                               // make sure that re-using the options doesn't screw things around
+                               s.jsonpCallback = originalSettings.jsonpCallback;
+
+                               // save the callback name for future use
+                               oldCallbacks.push( callbackName );
+                       }
+
+                       // Call if it was a function and we have a response
+                       if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+                               overwritten( responseContainer[ 0 ] );
+                       }
+
+                       responseContainer = overwritten = undefined;
+               });
+
+               // Delegate to script
+               return "script";
+       }
+});
+// Install script dataType
+jQuery.ajaxSetup({
+       accepts: {
+               script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
+       },
+       contents: {
+               script: /javascript|ecmascript/
+       },
+       converters: {
+               "text script": function( text ) {
+                       jQuery.globalEval( text );
+                       return text;
+               }
+       }
+});
+
+// Handle cache's special case and global
+jQuery.ajaxPrefilter( "script", function( s ) {
+       if ( s.cache === undefined ) {
+               s.cache = false;
+       }
+       if ( s.crossDomain ) {
+               s.type = "GET";
+               s.global = false;
+       }
+});
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function(s) {
+
+       // This transport only deals with cross domain requests
+       if ( s.crossDomain ) {
+
+               var script,
+                       head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement;
+
+               return {
+
+                       send: function( _, callback ) {
+
+                               script = document.createElement( "script" );
+
+                               script.async = "async";
+
+                               if ( s.scriptCharset ) {
+                                       script.charset = s.scriptCharset;
+                               }
+
+                               script.src = s.url;
+
+                               // Attach handlers for all browsers
+                               script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+                                       if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+                                               // Handle memory leak in IE
+                                               script.onload = script.onreadystatechange = null;
+
+                                               // Remove the script
+                                               if ( head && script.parentNode ) {
+                                                       head.removeChild( script );
+                                               }
+
+                                               // Dereference the script
+                                               script = undefined;
+
+                                               // Callback if not abort
+                                               if ( !isAbort ) {
+                                                       callback( 200, "success" );
+                                               }
+                                       }
+                               };
+                               // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
+                               // This arises when a base node is used (#2709 and #4378).
+                               head.insertBefore( script, head.firstChild );
+                       },
+
+                       abort: function() {
+                               if ( script ) {
+                                       script.onload( 0, 1 );
+                               }
+                       }
+               };
+       }
+});
+var xhrCallbacks,
+       // #5280: Internet Explorer will keep connections alive if we don't abort on unload
+       xhrOnUnloadAbort = window.ActiveXObject ? function() {
+               // Abort all pending requests
+               for ( var key in xhrCallbacks ) {
+                       xhrCallbacks[ key ]( 0, 1 );
+               }
+       } : false,
+       xhrId = 0;
+
+// Functions to create xhrs
+function createStandardXHR() {
+       try {
+               return new window.XMLHttpRequest();
+       } catch( e ) {}
+}
+
+function createActiveXHR() {
+       try {
+               return new window.ActiveXObject( "Microsoft.XMLHTTP" );
+       } catch( e ) {}
+}
+
+// Create the request object
+// (This is still attached to ajaxSettings for backward compatibility)
+jQuery.ajaxSettings.xhr = window.ActiveXObject ?
+       /* Microsoft failed to properly
+        * implement the XMLHttpRequest in IE7 (can't request local files),
+        * so we use the ActiveXObject when it is available
+        * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+        * we need a fallback.
+        */
+       function() {
+               return !this.isLocal && createStandardXHR() || createActiveXHR();
+       } :
+       // For all other browsers, use the standard XMLHttpRequest object
+       createStandardXHR;
+
+// Determine support properties
+(function( xhr ) {
+       jQuery.extend( jQuery.support, {
+               ajax: !!xhr,
+               cors: !!xhr && ( "withCredentials" in xhr )
+       });
+})( jQuery.ajaxSettings.xhr() );
+
+// Create transport if the browser can provide an xhr
+if ( jQuery.support.ajax ) {
+
+       jQuery.ajaxTransport(function( s ) {
+               // Cross domain only allowed if supported through XMLHttpRequest
+               if ( !s.crossDomain || jQuery.support.cors ) {
+
+                       var callback;
+
+                       return {
+                               send: function( headers, complete ) {
+
+                                       // Get a new xhr
+                                       var handle, i,
+                                               xhr = s.xhr();
+
+                                       // Open the socket
+                                       // Passing null username, generates a login popup on Opera (#2865)
+                                       if ( s.username ) {
+                                               xhr.open( s.type, s.url, s.async, s.username, s.password );
+                                       } else {
+                                               xhr.open( s.type, s.url, s.async );
+                                       }
+
+                                       // Apply custom fields if provided
+                                       if ( s.xhrFields ) {
+                                               for ( i in s.xhrFields ) {
+                                                       xhr[ i ] = s.xhrFields[ i ];
+                                               }
+                                       }
+
+                                       // Override mime type if needed
+                                       if ( s.mimeType && xhr.overrideMimeType ) {
+                                               xhr.overrideMimeType( s.mimeType );
+                                       }
+
+                                       // X-Requested-With header
+                                       // For cross-domain requests, seeing as conditions for a preflight are
+                                       // akin to a jigsaw puzzle, we simply never set it to be sure.
+                                       // (it can always be set on a per-request basis or even using ajaxSetup)
+                                       // For same-domain requests, won't change header if already provided.
+                                       if ( !s.crossDomain && !headers["X-Requested-With"] ) {
+                                               headers[ "X-Requested-With" ] = "XMLHttpRequest";
+                                       }
+
+                                       // Need an extra try/catch for cross domain requests in Firefox 3
+                                       try {
+                                               for ( i in headers ) {
+                                                       xhr.setRequestHeader( i, headers[ i ] );
+                                               }
+                                       } catch( _ ) {}
+
+                                       // Do send the request
+                                       // This may raise an exception which is actually
+                                       // handled in jQuery.ajax (so no try/catch here)
+                                       xhr.send( ( s.hasContent && s.data ) || null );
+
+                                       // Listener
+                                       callback = function( _, isAbort ) {
+
+                                               var status,
+                                                       statusText,
+                                                       responseHeaders,
+                                                       responses,
+                                                       xml;
+
+                                               // Firefox throws exceptions when accessing properties
+                                               // of an xhr when a network error occurred
+                                               // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
+                                               try {
+
+                                                       // Was never called and is aborted or complete
+                                                       if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
+
+                                                               // Only called once
+                                                               callback = undefined;
+
+                                                               // Do not keep as active anymore
+                                                               if ( handle ) {
+                                                                       xhr.onreadystatechange = jQuery.noop;
+                                                                       if ( xhrOnUnloadAbort ) {
+                                                                               delete xhrCallbacks[ handle ];
+                                                                       }
+                                                               }
+
+                                                               // If it's an abort
+                                                               if ( isAbort ) {
+                                                                       // Abort it manually if needed
+                                                                       if ( xhr.readyState !== 4 ) {
+                                                                               xhr.abort();
+                                                                       }
+                                                               } else {
+                                                                       status = xhr.status;
+                                                                       responseHeaders = xhr.getAllResponseHeaders();
+                                                                       responses = {};
+                                                                       xml = xhr.responseXML;
+
+                                                                       // Construct response list
+                                                                       if ( xml && xml.documentElement /* #4958 */ ) {
+                                                                               responses.xml = xml;
+                                                                       }
+
+                                                                       // When requesting binary data, IE6-9 will throw an exception
+                                                                       // on any attempt to access responseText (#11426)
+                                                                       try {
+                                                                               responses.text = xhr.responseText;
+                                                                       } catch( e ) {
+                                                                       }
+
+                                                                       // Firefox throws an exception when accessing
+                                                                       // statusText for faulty cross-domain requests
+                                                                       try {
+                                                                               statusText = xhr.statusText;
+                                                                       } catch( e ) {
+                                                                               // We normalize with Webkit giving an empty statusText
+                                                                               statusText = "";
+                                                                       }
+
+                                                                       // Filter status for non standard behaviors
+
+                                                                       // If the request is local and we have data: assume a success
+                                                                       // (success with no data won't get notified, that's the best we
+                                                                       // can do given current implementations)
+                                                                       if ( !status && s.isLocal && !s.crossDomain ) {
+                                                                               status = responses.text ? 200 : 404;
+                                                                       // IE - #1450: sometimes returns 1223 when it should be 204
+                                                                       } else if ( status === 1223 ) {
+                                                                               status = 204;
+                                                                       }
+                                                               }
+                                                       }
+                                               } catch( firefoxAccessException ) {
+                                                       if ( !isAbort ) {
+                                                               complete( -1, firefoxAccessException );
+                                                       }
+                                               }
+
+                                               // Call complete if needed
+                                               if ( responses ) {
+                                                       complete( status, statusText, responses, responseHeaders );
+                                               }
+                                       };
+
+                                       if ( !s.async ) {
+                                               // if we're in sync mode we fire the callback
+                                               callback();
+                                       } else if ( xhr.readyState === 4 ) {
+                                               // (IE6 & IE7) if it's in cache and has been
+                                               // retrieved directly we need to fire the callback
+                                               setTimeout( callback, 0 );
+                                       } else {
+                                               handle = ++xhrId;
+                                               if ( xhrOnUnloadAbort ) {
+                                                       // Create the active xhrs callbacks list if needed
+                                                       // and attach the unload handler
+                                                       if ( !xhrCallbacks ) {
+                                                               xhrCallbacks = {};
+                                                               jQuery( window ).unload( xhrOnUnloadAbort );
+                                                       }
+                                                       // Add to list of active xhrs callbacks
+                                                       xhrCallbacks[ handle ] = callback;
+                                               }
+                                               xhr.onreadystatechange = callback;
+                                       }
+                               },
+
+                               abort: function() {
+                                       if ( callback ) {
+                                               callback(0,1);
+                                       }
+                               }
+                       };
+               }
+       });
+}
+var fxNow, timerId,
+       rfxtypes = /^(?:toggle|show|hide)$/,
+       rfxnum = new RegExp( "^(?:([-+])=|)(" + core_pnum + ")([a-z%]*)$", "i" ),
+       rrun = /queueHooks$/,
+       animationPrefilters = [ defaultPrefilter ],
+       tweeners = {
+               "*": [function( prop, value ) {
+                       var end, unit,
+                               tween = this.createTween( prop, value ),
+                               parts = rfxnum.exec( value ),
+                               target = tween.cur(),
+                               start = +target || 0,
+                               scale = 1,
+                               maxIterations = 20;
+
+                       if ( parts ) {
+                               end = +parts[2];
+                               unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+
+                               // We need to compute starting value
+                               if ( unit !== "px" && start ) {
+                                       // Iteratively approximate from a nonzero starting point
+                                       // Prefer the current property, because this process will be trivial if it uses the same units
+                                       // Fallback to end or a simple constant
+                                       start = jQuery.css( tween.elem, prop, true ) || end || 1;
+
+                                       do {
+                                               // If previous iteration zeroed out, double until we get *something*
+                                               // Use a string for doubling factor so we don't accidentally see scale as unchanged below
+                                               scale = scale || ".5";
+
+                                               // Adjust and apply
+                                               start = start / scale;
+                                               jQuery.style( tween.elem, prop, start + unit );
+
+                                       // Update scale, tolerating zero or NaN from tween.cur()
+                                       // And breaking the loop if scale is unchanged or perfect, or if we've just had enough
+                                       } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
+                               }
+
+                               tween.unit = unit;
+                               tween.start = start;
+                               // If a +=/-= token was provided, we're doing a relative animation
+                               tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end;
+                       }
+                       return tween;
+               }]
+       };
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+       setTimeout(function() {
+               fxNow = undefined;
+       }, 0 );
+       return ( fxNow = jQuery.now() );
+}
+
+function createTweens( animation, props ) {
+       jQuery.each( props, function( prop, value ) {
+               var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
+                       index = 0,
+                       length = collection.length;
+               for ( ; index < length; index++ ) {
+                       if ( collection[ index ].call( animation, prop, value ) ) {
+
+                               // we're done with this property
+                               return;
+                       }
+               }
+       });
+}
+
+function Animation( elem, properties, options ) {
+       var result,
+               index = 0,
+               tweenerIndex = 0,
+               length = animationPrefilters.length,
+               deferred = jQuery.Deferred().always( function() {
+                       // don't match elem in the :animated selector
+                       delete tick.elem;
+               }),
+               tick = function() {
+                       var currentTime = fxNow || createFxNow(),
+                               remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+                               // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
+                               temp = remaining / animation.duration || 0,
+                               percent = 1 - temp,
+                               index = 0,
+                               length = animation.tweens.length;
+
+                       for ( ; index < length ; index++ ) {
+                               animation.tweens[ index ].run( percent );
+                       }
+
+                       deferred.notifyWith( elem, [ animation, percent, remaining ]);
+
+                       if ( percent < 1 && length ) {
+                               return remaining;
+                       } else {
+                               deferred.resolveWith( elem, [ animation ] );
+                               return false;
+                       }
+               },
+               animation = deferred.promise({
+                       elem: elem,
+                       props: jQuery.extend( {}, properties ),
+                       opts: jQuery.extend( true, { specialEasing: {} }, options ),
+                       originalProperties: properties,
+                       originalOptions: options,
+                       startTime: fxNow || createFxNow(),
+                       duration: options.duration,
+                       tweens: [],
+                       createTween: function( prop, end, easing ) {
+                               var tween = jQuery.Tween( elem, animation.opts, prop, end,
+                                               animation.opts.specialEasing[ prop ] || animation.opts.easing );
+                               animation.tweens.push( tween );
+                               return tween;
+                       },
+                       stop: function( gotoEnd ) {
+                               var index = 0,
+                                       // if we are going to the end, we want to run all the tweens
+                                       // otherwise we skip this part
+                                       length = gotoEnd ? animation.tweens.length : 0;
+
+                               for ( ; index < length ; index++ ) {
+                                       animation.tweens[ index ].run( 1 );
+                               }
+
+                               // resolve when we played the last frame
+                               // otherwise, reject
+                               if ( gotoEnd ) {
+                                       deferred.resolveWith( elem, [ animation, gotoEnd ] );
+                               } else {
+                                       deferred.rejectWith( elem, [ animation, gotoEnd ] );
+                               }
+                               return this;
+                       }
+               }),
+               props = animation.props;
+
+       propFilter( props, animation.opts.specialEasing );
+
+       for ( ; index < length ; index++ ) {
+               result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
+               if ( result ) {
+                       return result;
+               }
+       }
+
+       createTweens( animation, props );
+
+       if ( jQuery.isFunction( animation.opts.start ) ) {
+               animation.opts.start.call( elem, animation );
+       }
+
+       jQuery.fx.timer(
+               jQuery.extend( tick, {
+                       anim: animation,
+                       queue: animation.opts.queue,
+                       elem: elem
+               })
+       );
+
+       // attach callbacks from options
+       return animation.progress( animation.opts.progress )
+               .done( animation.opts.done, animation.opts.complete )
+               .fail( animation.opts.fail )
+               .always( animation.opts.always );
+}
+
+function propFilter( props, specialEasing ) {
+       var index, name, easing, value, hooks;
+
+       // camelCase, specialEasing and expand cssHook pass
+       for ( index in props ) {
+               name = jQuery.camelCase( index );
+               easing = specialEasing[ name ];
+               value = props[ index ];
+               if ( jQuery.isArray( value ) ) {
+                       easing = value[ 1 ];
+                       value = props[ index ] = value[ 0 ];
+               }
+
+               if ( index !== name ) {
+                       props[ name ] = value;
+                       delete props[ index ];
+               }
+
+               hooks = jQuery.cssHooks[ name ];
+               if ( hooks && "expand" in hooks ) {
+                       value = hooks.expand( value );
+                       delete props[ name ];
+
+                       // not quite $.extend, this wont overwrite keys already present.
+                       // also - reusing 'index' from above because we have the correct "name"
+                       for ( index in value ) {
+                               if ( !( index in props ) ) {
+                                       props[ index ] = value[ index ];
+                                       specialEasing[ index ] = easing;
+                               }
+                       }
+               } else {
+                       specialEasing[ name ] = easing;
+               }
+       }
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+       tweener: function( props, callback ) {
+               if ( jQuery.isFunction( props ) ) {
+                       callback = props;
+                       props = [ "*" ];
+               } else {
+                       props = props.split(" ");
+               }
+
+               var prop,
+                       index = 0,
+                       length = props.length;
+
+               for ( ; index < length ; index++ ) {
+                       prop = props[ index ];
+                       tweeners[ prop ] = tweeners[ prop ] || [];
+                       tweeners[ prop ].unshift( callback );
+               }
+       },
+
+       prefilter: function( callback, prepend ) {
+               if ( prepend ) {
+                       animationPrefilters.unshift( callback );
+               } else {
+                       animationPrefilters.push( callback );
+               }
+       }
+});
+
+function defaultPrefilter( elem, props, opts ) {
+       var index, prop, value, length, dataShow, toggle, tween, hooks, oldfire,
+               anim = this,
+               style = elem.style,
+               orig = {},
+               handled = [],
+               hidden = elem.nodeType && isHidden( elem );
+
+       // handle queue: false promises
+       if ( !opts.queue ) {
+               hooks = jQuery._queueHooks( elem, "fx" );
+               if ( hooks.unqueued == null ) {
+                       hooks.unqueued = 0;
+                       oldfire = hooks.empty.fire;
+                       hooks.empty.fire = function() {
+                               if ( !hooks.unqueued ) {
+                                       oldfire();
+                               }
+                       };
+               }
+               hooks.unqueued++;
+
+               anim.always(function() {
+                       // doing this makes sure that the complete handler will be called
+                       // before this completes
+                       anim.always(function() {
+                               hooks.unqueued--;
+                               if ( !jQuery.queue( elem, "fx" ).length ) {
+                                       hooks.empty.fire();
+                               }
+                       });
+               });
+       }
+
+       // height/width overflow pass
+       if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
+               // Make sure that nothing sneaks out
+               // Record all 3 overflow attributes because IE does not
+               // change the overflow attribute when overflowX and
+               // overflowY are set to the same value
+               opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+               // Set display property to inline-block for height/width
+               // animations on inline elements that are having width/height animated
+               if ( jQuery.css( elem, "display" ) === "inline" &&
+                               jQuery.css( elem, "float" ) === "none" ) {
+
+                       // inline-level elements accept inline-block;
+                       // block-level elements need to be inline with layout
+                       if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) {
+                               style.display = "inline-block";
+
+                       } else {
+                               style.zoom = 1;
+                       }
+               }
+       }
+
+       if ( opts.overflow ) {
+               style.overflow = "hidden";
+               if ( !jQuery.support.shrinkWrapBlocks ) {
+                       anim.done(function() {
+                               style.overflow = opts.overflow[ 0 ];
+                               style.overflowX = opts.overflow[ 1 ];
+                               style.overflowY = opts.overflow[ 2 ];
+                       });
+               }
+       }
+
+
+       // show/hide pass
+       for ( index in props ) {
+               value = props[ index ];
+               if ( rfxtypes.exec( value ) ) {
+                       delete props[ index ];
+                       toggle = toggle || value === "toggle";
+                       if ( value === ( hidden ? "hide" : "show" ) ) {
+                               continue;
+                       }
+                       handled.push( index );
+               }
+       }
+
+       length = handled.length;
+       if ( length ) {
+               dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} );
+               if ( "hidden" in dataShow ) {
+                       hidden = dataShow.hidden;
+               }
+
+               // store state if its toggle - enables .stop().toggle() to "reverse"
+               if ( toggle ) {
+                       dataShow.hidden = !hidden;
+               }
+               if ( hidden ) {
+                       jQuery( elem ).show();
+               } else {
+                       anim.done(function() {
+                               jQuery( elem ).hide();
+                       });
+               }
+               anim.done(function() {
+                       var prop;
+                       jQuery.removeData( elem, "fxshow", true );
+                       for ( prop in orig ) {
+                               jQuery.style( elem, prop, orig[ prop ] );
+                       }
+               });
+               for ( index = 0 ; index < length ; index++ ) {
+                       prop = handled[ index ];
+                       tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 );
+                       orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop );
+
+                       if ( !( prop in dataShow ) ) {
+                               dataShow[ prop ] = tween.start;
+                               if ( hidden ) {
+                                       tween.end = tween.start;
+                                       tween.start = prop === "width" || prop === "height" ? 1 : 0;
+                               }
+                       }
+               }
+       }
+}
+
+function Tween( elem, options, prop, end, easing ) {
+       return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+       constructor: Tween,
+       init: function( elem, options, prop, end, easing, unit ) {
+               this.elem = elem;
+               this.prop = prop;
+               this.easing = easing || "swing";
+               this.options = options;
+               this.start = this.now = this.cur();
+               this.end = end;
+               this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+       },
+       cur: function() {
+               var hooks = Tween.propHooks[ this.prop ];
+
+               return hooks && hooks.get ?
+                       hooks.get( this ) :
+                       Tween.propHooks._default.get( this );
+       },
+       run: function( percent ) {
+               var eased,
+                       hooks = Tween.propHooks[ this.prop ];
+
+               if ( this.options.duration ) {
+                       this.pos = eased = jQuery.easing[ this.easing ](
+                               percent, this.options.duration * percent, 0, 1, this.options.duration
+                       );
+               } else {
+                       this.pos = eased = percent;
+               }
+               this.now = ( this.end - this.start ) * eased + this.start;
+
+               if ( this.options.step ) {
+                       this.options.step.call( this.elem, this.now, this );
+               }
+
+               if ( hooks && hooks.set ) {
+                       hooks.set( this );
+               } else {
+                       Tween.propHooks._default.set( this );
+               }
+               return this;
+       }
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+       _default: {
+               get: function( tween ) {
+                       var result;
+
+                       if ( tween.elem[ tween.prop ] != null &&
+                               (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
+                               return tween.elem[ tween.prop ];
+                       }
+
+                       // passing any value as a 4th parameter to .css will automatically
+                       // attempt a parseFloat and fallback to a string if the parse fails
+                       // so, simple values such as "10px" are parsed to Float.
+                       // complex values such as "rotate(1rad)" are returned as is.
+                       result = jQuery.css( tween.elem, tween.prop, false, "" );
+                       // Empty strings, null, undefined and "auto" are converted to 0.
+                       return !result || result === "auto" ? 0 : result;
+               },
+               set: function( tween ) {
+                       // use step hook for back compat - use cssHook if its there - use .style if its
+                       // available and use plain properties where available
+                       if ( jQuery.fx.step[ tween.prop ] ) {
+                               jQuery.fx.step[ tween.prop ]( tween );
+                       } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
+                               jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+                       } else {
+                               tween.elem[ tween.prop ] = tween.now;
+                       }
+               }
+       }
+};
+
+// Remove in 2.0 - this supports IE8's panic based approach
+// to setting things on disconnected nodes
+
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+       set: function( tween ) {
+               if ( tween.elem.nodeType && tween.elem.parentNode ) {
+                       tween.elem[ tween.prop ] = tween.now;
+               }
+       }
+};
+
+jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
+       var cssFn = jQuery.fn[ name ];
+       jQuery.fn[ name ] = function( speed, easing, callback ) {
+               return speed == null || typeof speed === "boolean" ||
+                       // special check for .toggle( handler, handler, ... )
+                       ( !i && jQuery.isFunction( speed ) && jQuery.isFunction( easing ) ) ?
+                       cssFn.apply( this, arguments ) :
+                       this.animate( genFx( name, true ), speed, easing, callback );
+       };
+});
+
+jQuery.fn.extend({
+       fadeTo: function( speed, to, easing, callback ) {
+
+               // show any hidden elements after setting opacity to 0
+               return this.filter( isHidden ).css( "opacity", 0 ).show()
+
+                       // animate to the value specified
+                       .end().animate({ opacity: to }, speed, easing, callback );
+       },
+       animate: function( prop, speed, easing, callback ) {
+               var empty = jQuery.isEmptyObject( prop ),
+                       optall = jQuery.speed( speed, easing, callback ),
+                       doAnimation = function() {
+                               // Operate on a copy of prop so per-property easing won't be lost
+                               var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+
+                               // Empty animations resolve immediately
+                               if ( empty ) {
+                                       anim.stop( true );
+                               }
+                       };
+
+               return empty || optall.queue === false ?
+                       this.each( doAnimation ) :
+                       this.queue( optall.queue, doAnimation );
+       },
+       stop: function( type, clearQueue, gotoEnd ) {
+               var stopQueue = function( hooks ) {
+                       var stop = hooks.stop;
+                       delete hooks.stop;
+                       stop( gotoEnd );
+               };
+
+               if ( typeof type !== "string" ) {
+                       gotoEnd = clearQueue;
+                       clearQueue = type;
+                       type = undefined;
+               }
+               if ( clearQueue && type !== false ) {
+                       this.queue( type || "fx", [] );
+               }
+
+               return this.each(function() {
+                       var dequeue = true,
+                               index = type != null && type + "queueHooks",
+                               timers = jQuery.timers,
+                               data = jQuery._data( this );
+
+                       if ( index ) {
+                               if ( data[ index ] && data[ index ].stop ) {
+                                       stopQueue( data[ index ] );
+                               }
+                       } else {
+                               for ( index in data ) {
+                                       if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+                                               stopQueue( data[ index ] );
+                                       }
+                               }
+                       }
+
+                       for ( index = timers.length; index--; ) {
+                               if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
+                                       timers[ index ].anim.stop( gotoEnd );
+                                       dequeue = false;
+                                       timers.splice( index, 1 );
+                               }
+                       }
+
+                       // start the next in the queue if the last step wasn't forced
+                       // timers currently will call their complete callbacks, which will dequeue
+                       // but only if they were gotoEnd
+                       if ( dequeue || !gotoEnd ) {
+                               jQuery.dequeue( this, type );
+                       }
+               });
+       }
+});
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+       var which,
+               attrs = { height: type },
+               i = 0;
+
+       // if we include width, step value is 1 to do all cssExpand values,
+       // if we don't include width, step value is 2 to skip over Left and Right
+       includeWidth = includeWidth? 1 : 0;
+       for( ; i < 4 ; i += 2 - includeWidth ) {
+               which = cssExpand[ i ];
+               attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+       }
+
+       if ( includeWidth ) {
+               attrs.opacity = attrs.width = type;
+       }
+
+       return attrs;
+}
+
+// Generate shortcuts for custom animations
+jQuery.each({
+       slideDown: genFx("show"),
+       slideUp: genFx("hide"),
+       slideToggle: genFx("toggle"),
+       fadeIn: { opacity: "show" },
+       fadeOut: { opacity: "hide" },
+       fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+       jQuery.fn[ name ] = function( speed, easing, callback ) {
+               return this.animate( props, speed, easing, callback );
+       };
+});
+
+jQuery.speed = function( speed, easing, fn ) {
+       var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+               complete: fn || !fn && easing ||
+                       jQuery.isFunction( speed ) && speed,
+               duration: speed,
+               easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+       };
+
+       opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+               opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+       // normalize opt.queue - true/undefined/null -> "fx"
+       if ( opt.queue == null || opt.queue === true ) {
+               opt.queue = "fx";
+       }
+
+       // Queueing
+       opt.old = opt.complete;
+
+       opt.complete = function() {
+               if ( jQuery.isFunction( opt.old ) ) {
+                       opt.old.call( this );
+               }
+
+               if ( opt.queue ) {
+                       jQuery.dequeue( this, opt.queue );
+               }
+       };
+
+       return opt;
+};
+
+jQuery.easing = {
+       linear: function( p ) {
+               return p;
+       },
+       swing: function( p ) {
+               return 0.5 - Math.cos( p*Math.PI ) / 2;
+       }
+};
+
+jQuery.timers = [];
+jQuery.fx = Tween.prototype.init;
+jQuery.fx.tick = function() {
+       var timer,
+               timers = jQuery.timers,
+               i = 0;
+
+       fxNow = jQuery.now();
+
+       for ( ; i < timers.length; i++ ) {
+               timer = timers[ i ];
+               // Checks the timer has not already been removed
+               if ( !timer() && timers[ i ] === timer ) {
+                       timers.splice( i--, 1 );
+               }
+       }
+
+       if ( !timers.length ) {
+               jQuery.fx.stop();
+       }
+       fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+       if ( timer() && jQuery.timers.push( timer ) && !timerId ) {
+               timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
+       }
+};
+
+jQuery.fx.interval = 13;
+
+jQuery.fx.stop = function() {
+       clearInterval( timerId );
+       timerId = null;
+};
+
+jQuery.fx.speeds = {
+       slow: 600,
+       fast: 200,
+       // Default speed
+       _default: 400
+};
+
+// Back Compat <1.8 extension point
+jQuery.fx.step = {};
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+       jQuery.expr.filters.animated = function( elem ) {
+               return jQuery.grep(jQuery.timers, function( fn ) {
+                       return elem === fn.elem;
+               }).length;
+       };
+}
+var rroot = /^(?:body|html)$/i;
+
+jQuery.fn.offset = function( options ) {
+       if ( arguments.length ) {
+               return options === undefined ?
+                       this :
+                       this.each(function( i ) {
+                               jQuery.offset.setOffset( this, options, i );
+                       });
+       }
+
+       var docElem, body, win, clientTop, clientLeft, scrollTop, scrollLeft,
+               box = { top: 0, left: 0 },
+               elem = this[ 0 ],
+               doc = elem && elem.ownerDocument;
+
+       if ( !doc ) {
+               return;
+       }
+
+       if ( (body = doc.body) === elem ) {
+               return jQuery.offset.bodyOffset( elem );
+       }
+
+       docElem = doc.documentElement;
+
+       // Make sure it's not a disconnected DOM node
+       if ( !jQuery.contains( docElem, elem ) ) {
+               return box;
+       }
+
+       // If we don't have gBCR, just use 0,0 rather than error
+       // BlackBerry 5, iOS 3 (original iPhone)
+       if ( typeof elem.getBoundingClientRect !== "undefined" ) {
+               box = elem.getBoundingClientRect();
+       }
+       win = getWindow( doc );
+       clientTop  = docElem.clientTop  || body.clientTop  || 0;
+       clientLeft = docElem.clientLeft || body.clientLeft || 0;
+       scrollTop  = win.pageYOffset || docElem.scrollTop;
+       scrollLeft = win.pageXOffset || docElem.scrollLeft;
+       return {
+               top: box.top  + scrollTop  - clientTop,
+               left: box.left + scrollLeft - clientLeft
+       };
+};
+
+jQuery.offset = {
+
+       bodyOffset: function( body ) {
+               var top = body.offsetTop,
+                       left = body.offsetLeft;
+
+               if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) {
+                       top  += parseFloat( jQuery.css(body, "marginTop") ) || 0;
+                       left += parseFloat( jQuery.css(body, "marginLeft") ) || 0;
+               }
+
+               return { top: top, left: left };
+       },
+
+       setOffset: function( elem, options, i ) {
+               var position = jQuery.css( elem, "position" );
+
+               // set position first, in-case top/left are set even on static elem
+               if ( position === "static" ) {
+                       elem.style.position = "relative";
+               }
+
+               var curElem = jQuery( elem ),
+                       curOffset = curElem.offset(),
+                       curCSSTop = jQuery.css( elem, "top" ),
+                       curCSSLeft = jQuery.css( elem, "left" ),
+                       calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
+                       props = {}, curPosition = {}, curTop, curLeft;
+
+               // need to be able to calculate position if either top or left is auto and position is either absolute or fixed
+               if ( calculatePosition ) {
+                       curPosition = curElem.position();
+                       curTop = curPosition.top;
+                       curLeft = curPosition.left;
+               } else {
+                       curTop = parseFloat( curCSSTop ) || 0;
+                       curLeft = parseFloat( curCSSLeft ) || 0;
+               }
+
+               if ( jQuery.isFunction( options ) ) {
+                       options = options.call( elem, i, curOffset );
+               }
+
+               if ( options.top != null ) {
+                       props.top = ( options.top - curOffset.top ) + curTop;
+               }
+               if ( options.left != null ) {
+                       props.left = ( options.left - curOffset.left ) + curLeft;
+               }
+
+               if ( "using" in options ) {
+                       options.using.call( elem, props );
+               } else {
+                       curElem.css( props );
+               }
+       }
+};
+
+
+jQuery.fn.extend({
+
+       position: function() {
+               if ( !this[0] ) {
+                       return;
+               }
+
+               var elem = this[0],
+
+               // Get *real* offsetParent
+               offsetParent = this.offsetParent(),
+
+               // Get correct offsets
+               offset       = this.offset(),
+               parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+               // Subtract element margins
+               // note: when an element has margin: auto the offsetLeft and marginLeft
+               // are the same in Safari causing offset.left to incorrectly be 0
+               offset.top  -= parseFloat( jQuery.css(elem, "marginTop") ) || 0;
+               offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0;
+
+               // Add offsetParent borders
+               parentOffset.top  += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0;
+               parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0;
+
+               // Subtract the two offsets
+               return {
+                       top:  offset.top  - parentOffset.top,
+                       left: offset.left - parentOffset.left
+               };
+       },
+
+       offsetParent: function() {
+               return this.map(function() {
+                       var offsetParent = this.offsetParent || document.body;
+                       while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
+                               offsetParent = offsetParent.offsetParent;
+                       }
+                       return offsetParent || document.body;
+               });
+       }
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) {
+       var top = /Y/.test( prop );
+
+       jQuery.fn[ method ] = function( val ) {
+               return jQuery.access( this, function( elem, method, val ) {
+                       var win = getWindow( elem );
+
+                       if ( val === undefined ) {
+                               return win ? (prop in win) ? win[ prop ] :
+                                       win.document.documentElement[ method ] :
+                                       elem[ method ];
+                       }
+
+                       if ( win ) {
+                               win.scrollTo(
+                                       !top ? val : jQuery( win ).scrollLeft(),
+                                        top ? val : jQuery( win ).scrollTop()
+                               );
+
+                       } else {
+                               elem[ method ] = val;
+                       }
+               }, method, val, arguments.length, null );
+       };
+});
+
+function getWindow( elem ) {
+       return jQuery.isWindow( elem ) ?
+               elem :
+               elem.nodeType === 9 ?
+                       elem.defaultView || elem.parentWindow :
+                       false;
+}
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+       jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
+               // margin is only for outerHeight, outerWidth
+               jQuery.fn[ funcName ] = function( margin, value ) {
+                       var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+                               extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+                       return jQuery.access( this, function( elem, type, value ) {
+                               var doc;
+
+                               if ( jQuery.isWindow( elem ) ) {
+                                       // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
+                                       // isn't a whole lot we can do. See pull request at this URL for discussion:
+                                       // https://github.com/jquery/jquery/pull/764
+                                       return elem.document.documentElement[ "client" + name ];
+                               }
+
+                               // Get document width or height
+                               if ( elem.nodeType === 9 ) {
+                                       doc = elem.documentElement;
+
+                                       // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest
+                                       // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.
+                                       return Math.max(
+                                               elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+                                               elem.body[ "offset" + name ], doc[ "offset" + name ],
+                                               doc[ "client" + name ]
+                                       );
+                               }
+
+                               return value === undefined ?
+                                       // Get width or height on the element, requesting but not forcing parseFloat
+                                       jQuery.css( elem, type, value, extra ) :
+
+                                       // Set width or height on the element
+                                       jQuery.style( elem, type, value, extra );
+                       }, type, chainable ? margin : undefined, chainable, null );
+               };
+       });
+});
+// Expose jQuery to the global object
+window.jQuery = window.$ = jQuery;
+
+// Expose jQuery as an AMD module, but only for AMD loaders that
+// understand the issues with loading multiple versions of jQuery
+// in a page that all might call define(). The loader will indicate
+// they have special allowances for multiple jQuery versions by
+// specifying define.amd.jQuery = true. Register as a named module,
+// since jQuery can be concatenated with other files that may use define,
+// but not use a proper concatenation script that understands anonymous
+// AMD modules. A named AMD is safest and most robust way to register.
+// Lowercase jquery is used because AMD module names are derived from
+// file names, and jQuery is normally delivered in a lowercase file name.
+// Do this after creating the global so that if an AMD module wants to call
+// noConflict to hide this version of jQuery, it will work.
+if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
+       define( "jquery", [], function () { return jQuery; } );
+}
+
+})( window );
diff --git a/Android/webViewMarker/src/main/assets/rangy-core.js b/Android/webViewMarker/src/main/assets/rangy-core.js
new file mode 100755 (executable)
index 0000000..a30dd5a
--- /dev/null
@@ -0,0 +1,3224 @@
+/**\r
+ * @license Rangy, a cross-browser JavaScript range and selection library\r
+ * http://code.google.com/p/rangy/\r
+ *\r
+ * Copyright 2012, Tim Down\r
+ * Licensed under the MIT license.\r
+ * Version: 1.2.3\r
+ * Build date: 26 February 2012\r
+ */\r
+window['rangy'] = (function() {\r
+\r
+\r
+    var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";\r
+\r
+    var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",\r
+        "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];\r
+\r
+    var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",\r
+        "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",\r
+        "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];\r
+\r
+    var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];\r
+\r
+    // Subset of TextRange's full set of methods that we're interested in\r
+    var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",\r
+        "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];\r
+\r
+    /*----------------------------------------------------------------------------------------------------------------*/\r
+\r
+    // Trio of functions taken from Peter Michaux's article:\r
+    // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting\r
+    function isHostMethod(o, p) {\r
+        var t = typeof o[p];\r
+        return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";\r
+    }\r
+\r
+    function isHostObject(o, p) {\r
+        return !!(typeof o[p] == OBJECT && o[p]);\r
+    }\r
+\r
+    function isHostProperty(o, p) {\r
+        return typeof o[p] != UNDEFINED;\r
+    }\r
+\r
+    // Creates a convenience function to save verbose repeated calls to tests functions\r
+    function createMultiplePropertyTest(testFunc) {\r
+        return function(o, props) {\r
+            var i = props.length;\r
+            while (i--) {\r
+                if (!testFunc(o, props[i])) {\r
+                    return false;\r
+                }\r
+            }\r
+            return true;\r
+        };\r
+    }\r
+\r
+    // Next trio of functions are a convenience to save verbose repeated calls to previous two functions\r
+    var areHostMethods = createMultiplePropertyTest(isHostMethod);\r
+    var areHostObjects = createMultiplePropertyTest(isHostObject);\r
+    var areHostProperties = createMultiplePropertyTest(isHostProperty);\r
+\r
+    function isTextRange(range) {\r
+        return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);\r
+    }\r
+\r
+    var api = {\r
+        version: "1.2.3",\r
+        initialized: false,\r
+        supported: true,\r
+\r
+        util: {\r
+            isHostMethod: isHostMethod,\r
+            isHostObject: isHostObject,\r
+            isHostProperty: isHostProperty,\r
+            areHostMethods: areHostMethods,\r
+            areHostObjects: areHostObjects,\r
+            areHostProperties: areHostProperties,\r
+            isTextRange: isTextRange\r
+        },\r
+\r
+        features: {},\r
+\r
+        modules: {},\r
+        config: {\r
+            alertOnWarn: false,\r
+            preferTextRange: false\r
+        }\r
+    };\r
+\r
+    function fail(reason) {\r
+        window.alert("Rangy not supported in your browser. Reason: " + reason);\r
+        api.initialized = true;\r
+        api.supported = false;\r
+    }\r
+\r
+    api.fail = fail;\r
+\r
+    function warn(msg) {\r
+        var warningMessage = "Rangy warning: " + msg;\r
+        if (api.config.alertOnWarn) {\r
+            window.alert(warningMessage);\r
+        } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {\r
+            window.console.log(warningMessage);\r
+        }\r
+    }\r
+\r
+    api.warn = warn;\r
+\r
+    if ({}.hasOwnProperty) {\r
+        api.util.extend = function(o, props) {\r
+            for (var i in props) {\r
+                if (props.hasOwnProperty(i)) {\r
+                    o[i] = props[i];\r
+                }\r
+            }\r
+        };\r
+    } else {\r
+        fail("hasOwnProperty not supported");\r
+    }\r
+\r
+    var initListeners = [];\r
+    var moduleInitializers = [];\r
+\r
+    // Initialization\r
+    function init() {\r
+        if (api.initialized) {\r
+            return;\r
+        }\r
+        var testRange;\r
+        var implementsDomRange = false, implementsTextRange = false;\r
+\r
+        // First, perform basic feature tests\r
+\r
+        if (isHostMethod(document, "createRange")) {\r
+            testRange = document.createRange();\r
+            if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {\r
+                implementsDomRange = true;\r
+            }\r
+            testRange.detach();\r
+        }\r
+\r
+        var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];\r
+\r
+        if (body && isHostMethod(body, "createTextRange")) {\r
+            testRange = body.createTextRange();\r
+            if (isTextRange(testRange)) {\r
+                implementsTextRange = true;\r
+            }\r
+        }\r
+\r
+        if (!implementsDomRange && !implementsTextRange) {\r
+            fail("Neither Range nor TextRange are implemented");\r
+        }\r
+\r
+        api.initialized = true;\r
+        api.features = {\r
+            implementsDomRange: implementsDomRange,\r
+            implementsTextRange: implementsTextRange\r
+        };\r
+\r
+        // Initialize modules and call init listeners\r
+        var allListeners = moduleInitializers.concat(initListeners);\r
+        for (var i = 0, len = allListeners.length; i < len; ++i) {\r
+            try {\r
+                allListeners[i](api);\r
+            } catch (ex) {\r
+                if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {\r
+                    window.console.log("Init listener threw an exception. Continuing.", ex);\r
+                }\r
+\r
+            }\r
+        }\r
+    }\r
+\r
+    // Allow external scripts to initialize this library in case it's loaded after the document has loaded\r
+    api.init = init;\r
+\r
+    // Execute listener immediately if already initialized\r
+    api.addInitListener = function(listener) {\r
+        if (api.initialized) {\r
+            listener(api);\r
+        } else {\r
+            initListeners.push(listener);\r
+        }\r
+    };\r
+\r
+    var createMissingNativeApiListeners = [];\r
+\r
+    api.addCreateMissingNativeApiListener = function(listener) {\r
+        createMissingNativeApiListeners.push(listener);\r
+    };\r
+\r
+    function createMissingNativeApi(win) {\r
+        win = win || window;\r
+        init();\r
+\r
+        // Notify listeners\r
+        for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {\r
+            createMissingNativeApiListeners[i](win);\r
+        }\r
+    }\r
+\r
+    api.createMissingNativeApi = createMissingNativeApi;\r
+\r
+    /**\r
+     * @constructor\r
+     */\r
+    function Module(name) {\r
+        this.name = name;\r
+        this.initialized = false;\r
+        this.supported = false;\r
+    }\r
+\r
+    Module.prototype.fail = function(reason) {\r
+        this.initialized = true;\r
+        this.supported = false;\r
+\r
+        throw new Error("Module '" + this.name + "' failed to load: " + reason);\r
+    };\r
+\r
+    Module.prototype.warn = function(msg) {\r
+        api.warn("Module " + this.name + ": " + msg);\r
+    };\r
+\r
+    Module.prototype.createError = function(msg) {\r
+        return new Error("Error in Rangy " + this.name + " module: " + msg);\r
+    };\r
+\r
+    api.createModule = function(name, initFunc) {\r
+        var module = new Module(name);\r
+        api.modules[name] = module;\r
+\r
+        moduleInitializers.push(function(api) {\r
+            initFunc(api, module);\r
+            module.initialized = true;\r
+            module.supported = true;\r
+        });\r
+    };\r
+\r
+    api.requireModules = function(modules) {\r
+        for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {\r
+            moduleName = modules[i];\r
+            module = api.modules[moduleName];\r
+            if (!module || !(module instanceof Module)) {\r
+                throw new Error("Module '" + moduleName + "' not found");\r
+            }\r
+            if (!module.supported) {\r
+                throw new Error("Module '" + moduleName + "' not supported");\r
+            }\r
+        }\r
+    };\r
+\r
+    /*----------------------------------------------------------------------------------------------------------------*/\r
+\r
+    // Wait for document to load before running tests\r
+\r
+    var docReady = false;\r
+\r
+    var loadHandler = function(e) {\r
+\r
+        if (!docReady) {\r
+            docReady = true;\r
+            if (!api.initialized) {\r
+                init();\r
+            }\r
+        }\r
+    };\r
+\r
+    // Test whether we have window and document objects that we will need\r
+    if (typeof window == UNDEFINED) {\r
+        fail("No window found");\r
+        return;\r
+    }\r
+    if (typeof document == UNDEFINED) {\r
+        fail("No document found");\r
+        return;\r
+    }\r
+\r
+    if (isHostMethod(document, "addEventListener")) {\r
+        document.addEventListener("DOMContentLoaded", loadHandler, false);\r
+    }\r
+\r
+    // Add a fallback in case the DOMContentLoaded event isn't supported\r
+    if (isHostMethod(window, "addEventListener")) {\r
+        window.addEventListener("load", loadHandler, false);\r
+    } else if (isHostMethod(window, "attachEvent")) {\r
+        window.attachEvent("onload", loadHandler);\r
+    } else {\r
+        fail("Window does not have required addEventListener or attachEvent method");\r
+    }\r
+\r
+    return api;\r
+})();\r
+rangy.createModule("DomUtil", function(api, module) {\r
+\r
+    var UNDEF = "undefined";\r
+    var util = api.util;\r
+\r
+    // Perform feature tests\r
+    if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {\r
+        module.fail("document missing a Node creation method");\r
+    }\r
+\r
+    if (!util.isHostMethod(document, "getElementsByTagName")) {\r
+        module.fail("document missing getElementsByTagName method");\r
+    }\r
+\r
+    var el = document.createElement("div");\r
+    if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||\r
+            !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {\r
+        module.fail("Incomplete Element implementation");\r
+    }\r
+\r
+    // innerHTML is required for Range's createContextualFragment method\r
+    if (!util.isHostProperty(el, "innerHTML")) {\r
+        module.fail("Element is missing innerHTML property");\r
+    }\r
+\r
+    var textNode = document.createTextNode("test");\r
+    if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||\r
+            !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||\r
+            !util.areHostProperties(textNode, ["data"]))) {\r
+        module.fail("Incomplete Text Node implementation");\r
+    }\r
+\r
+    /*----------------------------------------------------------------------------------------------------------------*/\r
+\r
+    // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been\r
+    // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that\r
+    // contains just the document as a single element and the value searched for is the document.\r
+    var arrayContains = /*Array.prototype.indexOf ?\r
+        function(arr, val) {\r
+            return arr.indexOf(val) > -1;\r
+        }:*/\r
+\r
+        function(arr, val) {\r
+            var i = arr.length;\r
+            while (i--) {\r
+                if (arr[i] === val) {\r
+                    return true;\r
+                }\r
+            }\r
+            return false;\r
+        };\r
+\r
+    // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI\r
+    function isHtmlNamespace(node) {\r
+        var ns;\r
+        return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");\r
+    }\r
+\r
+    function parentElement(node) {\r
+        var parent = node.parentNode;\r
+        return (parent.nodeType == 1) ? parent : null;\r
+    }\r
+\r
+    function getNodeIndex(node) {\r
+        var i = 0;\r
+        while( (node = node.previousSibling) ) {\r
+            i++;\r
+        }\r
+        return i;\r
+    }\r
+\r
+    function getNodeLength(node) {\r
+        var childNodes;\r
+        return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);\r
+    }\r
+\r
+    function getCommonAncestor(node1, node2) {\r
+        var ancestors = [], n;\r
+        for (n = node1; n; n = n.parentNode) {\r
+            ancestors.push(n);\r
+        }\r
+\r
+        for (n = node2; n; n = n.parentNode) {\r
+            if (arrayContains(ancestors, n)) {\r
+                return n;\r
+            }\r
+        }\r
+\r
+        return null;\r
+    }\r
+\r
+    function isAncestorOf(ancestor, descendant, selfIsAncestor) {\r
+        var n = selfIsAncestor ? descendant : descendant.parentNode;\r
+        while (n) {\r
+            if (n === ancestor) {\r
+                return true;\r
+            } else {\r
+                n = n.parentNode;\r
+            }\r
+        }\r
+        return false;\r
+    }\r
+\r
+    function getClosestAncestorIn(node, ancestor, selfIsAncestor) {\r
+        var p, n = selfIsAncestor ? node : node.parentNode;\r
+        while (n) {\r
+            p = n.parentNode;\r
+            if (p === ancestor) {\r
+                return n;\r
+            }\r
+            n = p;\r
+        }\r
+        return null;\r
+    }\r
+\r
+    function isCharacterDataNode(node) {\r
+        var t = node.nodeType;\r
+        return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment\r
+    }\r
+\r
+    function insertAfter(node, precedingNode) {\r
+        var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;\r
+        if (nextNode) {\r
+            parent.insertBefore(node, nextNode);\r
+        } else {\r
+            parent.appendChild(node);\r
+        }\r
+        return node;\r
+    }\r
+\r
+    // Note that we cannot use splitText() because it is bugridden in IE 9.\r
+    function splitDataNode(node, index) {\r
+        var newNode = node.cloneNode(false);\r
+        newNode.deleteData(0, index);\r
+        node.deleteData(index, node.length - index);\r
+        insertAfter(newNode, node);\r
+        return newNode;\r
+    }\r
+\r
+    function getDocument(node) {\r
+        if (node.nodeType == 9) {\r
+            return node;\r
+        } else if (typeof node.ownerDocument != UNDEF) {\r
+            return node.ownerDocument;\r
+        } else if (typeof node.document != UNDEF) {\r
+            return node.document;\r
+        } else if (node.parentNode) {\r
+            return getDocument(node.parentNode);\r
+        } else {\r
+            throw new Error("getDocument: no document found for node");\r
+        }\r
+    }\r
+\r
+    function getWindow(node) {\r
+        var doc = getDocument(node);\r
+        if (typeof doc.defaultView != UNDEF) {\r
+            return doc.defaultView;\r
+        } else if (typeof doc.parentWindow != UNDEF) {\r
+            return doc.parentWindow;\r
+        } else {\r
+            throw new Error("Cannot get a window object for node");\r
+        }\r
+    }\r
+\r
+    function getIframeDocument(iframeEl) {\r
+        if (typeof iframeEl.contentDocument != UNDEF) {\r
+            return iframeEl.contentDocument;\r
+        } else if (typeof iframeEl.contentWindow != UNDEF) {\r
+            return iframeEl.contentWindow.document;\r
+        } else {\r
+            throw new Error("getIframeWindow: No Document object found for iframe element");\r
+        }\r
+    }\r
+\r
+    function getIframeWindow(iframeEl) {\r
+        if (typeof iframeEl.contentWindow != UNDEF) {\r
+            return iframeEl.contentWindow;\r
+        } else if (typeof iframeEl.contentDocument != UNDEF) {\r
+            return iframeEl.contentDocument.defaultView;\r
+        } else {\r
+            throw new Error("getIframeWindow: No Window object found for iframe element");\r
+        }\r
+    }\r
+\r
+    function getBody(doc) {\r
+        return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];\r
+    }\r
+\r
+    function getRootContainer(node) {\r
+        var parent;\r
+        while ( (parent = node.parentNode) ) {\r
+            node = parent;\r
+        }\r
+        return node;\r
+    }\r
+\r
+    function comparePoints(nodeA, offsetA, nodeB, offsetB) {\r
+        // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing\r
+        var nodeC, root, childA, childB, n;\r
+        if (nodeA == nodeB) {\r
+\r
+            // Case 1: nodes are the same\r
+            return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;\r
+        } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {\r
+\r
+            // Case 2: node C (container B or an ancestor) is a child node of A\r
+            return offsetA <= getNodeIndex(nodeC) ? -1 : 1;\r
+        } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {\r
+\r
+            // Case 3: node C (container A or an ancestor) is a child node of B\r
+            return getNodeIndex(nodeC) < offsetB  ? -1 : 1;\r
+        } else {\r
+\r
+            // Case 4: containers are siblings or descendants of siblings\r
+            root = getCommonAncestor(nodeA, nodeB);\r
+            childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);\r
+            childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);\r
+\r
+            if (childA === childB) {\r
+                // This shouldn't be possible\r
+\r
+                throw new Error("comparePoints got to case 4 and childA and childB are the same!");\r
+            } else {\r
+                n = root.firstChild;\r
+                while (n) {\r
+                    if (n === childA) {\r
+                        return -1;\r
+                    } else if (n === childB) {\r
+                        return 1;\r
+                    }\r
+                    n = n.nextSibling;\r
+                }\r
+                throw new Error("Should not be here!");\r
+            }\r
+        }\r
+    }\r
+\r
+    function fragmentFromNodeChildren(node) {\r
+        var fragment = getDocument(node).createDocumentFragment(), child;\r
+        while ( (child = node.firstChild) ) {\r
+            fragment.appendChild(child);\r
+        }\r
+        return fragment;\r
+    }\r
+\r
+    function inspectNode(node) {\r
+        if (!node) {\r
+            return "[No node]";\r
+        }\r
+        if (isCharacterDataNode(node)) {\r
+            return '"' + node.data + '"';\r
+        } else if (node.nodeType == 1) {\r
+            var idAttr = node.id ? ' id="' + node.id + '"' : "";\r
+            return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";\r
+        } else {\r
+            return node.nodeName;\r
+        }\r
+    }\r
+\r
+    /**\r
+     * @constructor\r
+     */\r
+    function NodeIterator(root) {\r
+        this.root = root;\r
+        this._next = root;\r
+    }\r
+\r
+    NodeIterator.prototype = {\r
+        _current: null,\r
+\r
+        hasNext: function() {\r
+            return !!this._next;\r
+        },\r
+\r
+        next: function() {\r
+            var n = this._current = this._next;\r
+            var child, next;\r
+            if (this._current) {\r
+                child = n.firstChild;\r
+                if (child) {\r
+                    this._next = child;\r
+                } else {\r
+                    next = null;\r
+                    while ((n !== this.root) && !(next = n.nextSibling)) {\r
+                        n = n.parentNode;\r
+                    }\r
+                    this._next = next;\r
+                }\r
+            }\r
+            return this._current;\r
+        },\r
+\r
+        detach: function() {\r
+            this._current = this._next = this.root = null;\r
+        }\r
+    };\r
+\r
+    function createIterator(root) {\r
+        return new NodeIterator(root);\r
+    }\r
+\r
+    /**\r
+     * @constructor\r
+     */\r
+    function DomPosition(node, offset) {\r
+        this.node = node;\r
+        this.offset = offset;\r
+    }\r
+\r
+    DomPosition.prototype = {\r
+        equals: function(pos) {\r
+            return this.node === pos.node & this.offset == pos.offset;\r
+        },\r
+\r
+        inspect: function() {\r
+            return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";\r
+        }\r
+    };\r
+\r
+    /**\r
+     * @constructor\r
+     */\r
+    function DOMException(codeName) {\r
+        this.code = this[codeName];\r
+        this.codeName = codeName;\r
+        this.message = "DOMException: " + this.codeName;\r
+    }\r
+\r
+    DOMException.prototype = {\r
+        INDEX_SIZE_ERR: 1,\r
+        HIERARCHY_REQUEST_ERR: 3,\r
+        WRONG_DOCUMENT_ERR: 4,\r
+        NO_MODIFICATION_ALLOWED_ERR: 7,\r
+        NOT_FOUND_ERR: 8,\r
+        NOT_SUPPORTED_ERR: 9,\r
+        INVALID_STATE_ERR: 11\r
+    };\r
+\r
+    DOMException.prototype.toString = function() {\r
+        return this.message;\r
+    };\r
+\r
+    api.dom = {\r
+        arrayContains: arrayContains,\r
+        isHtmlNamespace: isHtmlNamespace,\r
+        parentElement: parentElement,\r
+        getNodeIndex: getNodeIndex,\r
+        getNodeLength: getNodeLength,\r
+        getCommonAncestor: getCommonAncestor,\r
+        isAncestorOf: isAncestorOf,\r
+        getClosestAncestorIn: getClosestAncestorIn,\r
+        isCharacterDataNode: isCharacterDataNode,\r
+        insertAfter: insertAfter,\r
+        splitDataNode: splitDataNode,\r
+        getDocument: getDocument,\r
+        getWindow: getWindow,\r
+        getIframeWindow: getIframeWindow,\r
+        getIframeDocument: getIframeDocument,\r
+        getBody: getBody,\r
+        getRootContainer: getRootContainer,\r
+        comparePoints: comparePoints,\r
+        inspectNode: inspectNode,\r
+        fragmentFromNodeChildren: fragmentFromNodeChildren,\r
+        createIterator: createIterator,\r
+        DomPosition: DomPosition\r
+    };\r
+\r
+    api.DOMException = DOMException;\r
+});rangy.createModule("DomRange", function(api, module) {
+    api.requireModules( ["DomUtil"] );
+
+
+    var dom = api.dom;
+    var DomPosition = dom.DomPosition;
+    var DOMException = api.DOMException;
+    
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Utility functions
+
+    function isNonTextPartiallySelected(node, range) {
+        return (node.nodeType != 3) &&
+               (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
+    }
+
+    function getRangeDocument(range) {
+        return dom.getDocument(range.startContainer);
+    }
+
+    function dispatchEvent(range, type, args) {
+        var listeners = range._listeners[type];
+        if (listeners) {
+            for (var i = 0, len = listeners.length; i < len; ++i) {
+                listeners[i].call(range, {target: range, args: args});
+            }
+        }
+    }
+
+    function getBoundaryBeforeNode(node) {
+        return new DomPosition(node.parentNode, dom.getNodeIndex(node));
+    }
+
+    function getBoundaryAfterNode(node) {
+        return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
+    }
+
+    function insertNodeAtPosition(node, n, o) {
+        var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
+        if (dom.isCharacterDataNode(n)) {
+            if (o == n.length) {
+                dom.insertAfter(node, n);
+            } else {
+                n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
+            }
+        } else if (o >= n.childNodes.length) {
+            n.appendChild(node);
+        } else {
+            n.insertBefore(node, n.childNodes[o]);
+        }
+        return firstNodeInserted;
+    }
+
+    function cloneSubtree(iterator) {
+        var partiallySelected;
+        for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
+            partiallySelected = iterator.isPartiallySelectedSubtree();
+
+            node = node.cloneNode(!partiallySelected);
+            if (partiallySelected) {
+                subIterator = iterator.getSubtreeIterator();
+                node.appendChild(cloneSubtree(subIterator));
+                subIterator.detach(true);
+            }
+
+            if (node.nodeType == 10) { // DocumentType
+                throw new DOMException("HIERARCHY_REQUEST_ERR");
+            }
+            frag.appendChild(node);
+        }
+        return frag;
+    }
+
+    function iterateSubtree(rangeIterator, func, iteratorState) {
+        var it, n;
+        iteratorState = iteratorState || { stop: false };
+        for (var node, subRangeIterator; node = rangeIterator.next(); ) {
+            //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
+            if (rangeIterator.isPartiallySelectedSubtree()) {
+                // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
+                // node selected by the Range.
+                if (func(node) === false) {
+                    iteratorState.stop = true;
+                    return;
+                } else {
+                    subRangeIterator = rangeIterator.getSubtreeIterator();
+                    iterateSubtree(subRangeIterator, func, iteratorState);
+                    subRangeIterator.detach(true);
+                    if (iteratorState.stop) {
+                        return;
+                    }
+                }
+            } else {
+                // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
+                // descendant
+                it = dom.createIterator(node);
+                while ( (n = it.next()) ) {
+                    if (func(n) === false) {
+                        iteratorState.stop = true;
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
+    function deleteSubtree(iterator) {
+        var subIterator;
+        while (iterator.next()) {
+            if (iterator.isPartiallySelectedSubtree()) {
+                subIterator = iterator.getSubtreeIterator();
+                deleteSubtree(subIterator);
+                subIterator.detach(true);
+            } else {
+                iterator.remove();
+            }
+        }
+    }
+
+    function extractSubtree(iterator) {
+
+        for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
+
+
+            if (iterator.isPartiallySelectedSubtree()) {
+                node = node.cloneNode(false);
+                subIterator = iterator.getSubtreeIterator();
+                node.appendChild(extractSubtree(subIterator));
+                subIterator.detach(true);
+            } else {
+                iterator.remove();
+            }
+            if (node.nodeType == 10) { // DocumentType
+                throw new DOMException("HIERARCHY_REQUEST_ERR");
+            }
+            frag.appendChild(node);
+        }
+        return frag;
+    }
+
+    function getNodesInRange(range, nodeTypes, filter) {
+        //log.info("getNodesInRange, " + nodeTypes.join(","));
+        var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
+        var filterExists = !!filter;
+        if (filterNodeTypes) {
+            regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
+        }
+
+        var nodes = [];
+        iterateSubtree(new RangeIterator(range, false), function(node) {
+            if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
+                nodes.push(node);
+            }
+        });
+        return nodes;
+    }
+
+    function inspect(range) {
+        var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
+        return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
+                dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
+    }
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
+
+    /**
+     * @constructor
+     */
+    function RangeIterator(range, clonePartiallySelectedTextNodes) {
+        this.range = range;
+        this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
+
+
+
+        if (!range.collapsed) {
+            this.sc = range.startContainer;
+            this.so = range.startOffset;
+            this.ec = range.endContainer;
+            this.eo = range.endOffset;
+            var root = range.commonAncestorContainer;
+
+            if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
+                this.isSingleCharacterDataNode = true;
+                this._first = this._last = this._next = this.sc;
+            } else {
+                this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
+                    this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
+                this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
+                    this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
+            }
+
+        }
+    }
+
+    RangeIterator.prototype = {
+        _current: null,
+        _next: null,
+        _first: null,
+        _last: null,
+        isSingleCharacterDataNode: false,
+
+        reset: function() {
+            this._current = null;
+            this._next = this._first;
+        },
+
+        hasNext: function() {
+            return !!this._next;
+        },
+
+        next: function() {
+            // Move to next node
+            var current = this._current = this._next;
+            if (current) {
+                this._next = (current !== this._last) ? current.nextSibling : null;
+
+                // Check for partially selected text nodes
+                if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
+                    if (current === this.ec) {
+
+                        (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
+                    }
+                    if (this._current === this.sc) {
+
+                        (current = current.cloneNode(true)).deleteData(0, this.so);
+                    }
+                }
+            }
+
+            return current;
+        },
+
+        remove: function() {
+            var current = this._current, start, end;
+
+            if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
+                start = (current === this.sc) ? this.so : 0;
+                end = (current === this.ec) ? this.eo : current.length;
+                if (start != end) {
+                    current.deleteData(start, end - start);
+                }
+            } else {
+                if (current.parentNode) {
+                    current.parentNode.removeChild(current);
+                } else {
+
+                }
+            }
+        },
+
+        // Checks if the current node is partially selected
+        isPartiallySelectedSubtree: function() {
+            var current = this._current;
+            return isNonTextPartiallySelected(current, this.range);
+        },
+
+        getSubtreeIterator: function() {
+            var subRange;
+            if (this.isSingleCharacterDataNode) {
+                subRange = this.range.cloneRange();
+                subRange.collapse();
+            } else {
+                subRange = new Range(getRangeDocument(this.range));
+                var current = this._current;
+                var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
+
+                if (dom.isAncestorOf(current, this.sc, true)) {
+                    startContainer = this.sc;
+                    startOffset = this.so;
+                }
+                if (dom.isAncestorOf(current, this.ec, true)) {
+                    endContainer = this.ec;
+                    endOffset = this.eo;
+                }
+
+                updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
+            }
+            return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
+        },
+
+        detach: function(detachRange) {
+            if (detachRange) {
+                this.range.detach();
+            }
+            this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
+        }
+    };
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Exceptions
+
+    /**
+     * @constructor
+     */
+    function RangeException(codeName) {
+        this.code = this[codeName];
+        this.codeName = codeName;
+        this.message = "RangeException: " + this.codeName;
+    }
+
+    RangeException.prototype = {
+        BAD_BOUNDARYPOINTS_ERR: 1,
+        INVALID_NODE_TYPE_ERR: 2
+    };
+
+    RangeException.prototype.toString = function() {
+        return this.message;
+    };
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    /**
+     * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
+     * TODO: Look into making this a proper iterator, not requiring preloading everything first
+     * @constructor
+     */
+    function RangeNodeIterator(range, nodeTypes, filter) {
+        this.nodes = getNodesInRange(range, nodeTypes, filter);
+        this._next = this.nodes[0];
+        this._position = 0;
+    }
+
+    RangeNodeIterator.prototype = {
+        _current: null,
+
+        hasNext: function() {
+            return !!this._next;
+        },
+
+        next: function() {
+            this._current = this._next;
+            this._next = this.nodes[ ++this._position ];
+            return this._current;
+        },
+
+        detach: function() {
+            this._current = this._next = this.nodes = null;
+        }
+    };
+
+    var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
+    var rootContainerNodeTypes = [2, 9, 11];
+    var readonlyNodeTypes = [5, 6, 10, 12];
+    var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
+    var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
+
+    function createAncestorFinder(nodeTypes) {
+        return function(node, selfIsAncestor) {
+            var t, n = selfIsAncestor ? node : node.parentNode;
+            while (n) {
+                t = n.nodeType;
+                if (dom.arrayContains(nodeTypes, t)) {
+                    return n;
+                }
+                n = n.parentNode;
+            }
+            return null;
+        };
+    }
+
+    var getRootContainer = dom.getRootContainer;
+    var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
+    var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
+    var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
+
+    function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
+        if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
+            throw new RangeException("INVALID_NODE_TYPE_ERR");
+        }
+    }
+
+    function assertNotDetached(range) {
+        if (!range.startContainer) {
+            throw new DOMException("INVALID_STATE_ERR");
+        }
+    }
+
+    function assertValidNodeType(node, invalidTypes) {
+        if (!dom.arrayContains(invalidTypes, node.nodeType)) {
+            throw new RangeException("INVALID_NODE_TYPE_ERR");
+        }
+    }
+
+    function assertValidOffset(node, offset) {
+        if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
+            throw new DOMException("INDEX_SIZE_ERR");
+        }
+    }
+
+    function assertSameDocumentOrFragment(node1, node2) {
+        if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
+            throw new DOMException("WRONG_DOCUMENT_ERR");
+        }
+    }
+
+    function assertNodeNotReadOnly(node) {
+        if (getReadonlyAncestor(node, true)) {
+            throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
+        }
+    }
+
+    function assertNode(node, codeName) {
+        if (!node) {
+            throw new DOMException(codeName);
+        }
+    }
+
+    function isOrphan(node) {
+        return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
+    }
+
+    function isValidOffset(node, offset) {
+        return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
+    }
+
+    function isRangeValid(range) {
+        return (!!range.startContainer && !!range.endContainer
+                && !isOrphan(range.startContainer)
+                && !isOrphan(range.endContainer)
+                && isValidOffset(range.startContainer, range.startOffset)
+                && isValidOffset(range.endContainer, range.endOffset));
+    }
+
+    function assertRangeValid(range) {
+        assertNotDetached(range);
+        if (!isRangeValid(range)) {
+            throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
+        }
+    }
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Test the browser's innerHTML support to decide how to implement createContextualFragment
+    var styleEl = document.createElement("style");
+    var htmlParsingConforms = false;
+    try {
+        styleEl.innerHTML = "<b>x</b>";
+        htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
+    } catch (e) {
+        // IE 6 and 7 throw
+    }
+
+    api.features.htmlParsingConforms = htmlParsingConforms;
+
+    var createContextualFragment = htmlParsingConforms ?
+
+        // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
+        // discussion and base code for this implementation at issue 67.
+        // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
+        // Thanks to Aleks Williams.
+        function(fragmentStr) {
+            // "Let node the context object's start's node."
+            var node = this.startContainer;
+            var doc = dom.getDocument(node);
+
+            // "If the context object's start's node is null, raise an INVALID_STATE_ERR
+            // exception and abort these steps."
+            if (!node) {
+                throw new DOMException("INVALID_STATE_ERR");
+            }
+
+            // "Let element be as follows, depending on node's interface:"
+            // Document, Document Fragment: null
+            var el = null;
+
+            // "Element: node"
+            if (node.nodeType == 1) {
+                el = node;
+
+            // "Text, Comment: node's parentElement"
+            } else if (dom.isCharacterDataNode(node)) {
+                el = dom.parentElement(node);
+            }
+
+            // "If either element is null or element's ownerDocument is an HTML document
+            // and element's local name is "html" and element's namespace is the HTML
+            // namespace"
+            if (el === null || (
+                el.nodeName == "HTML"
+                && dom.isHtmlNamespace(dom.getDocument(el).documentElement)
+                && dom.isHtmlNamespace(el)
+            )) {
+
+            // "let element be a new Element with "body" as its local name and the HTML
+            // namespace as its namespace.""
+                el = doc.createElement("body");
+            } else {
+                el = el.cloneNode(false);
+            }
+
+            // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
+            // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
+            // "In either case, the algorithm must be invoked with fragment as the input
+            // and element as the context element."
+            el.innerHTML = fragmentStr;
+
+            // "If this raises an exception, then abort these steps. Otherwise, let new
+            // children be the nodes returned."
+
+            // "Let fragment be a new DocumentFragment."
+            // "Append all new children to fragment."
+            // "Return fragment."
+            return dom.fragmentFromNodeChildren(el);
+        } :
+
+        // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
+        // previous versions of Rangy used (with the exception of using a body element rather than a div)
+        function(fragmentStr) {
+            assertNotDetached(this);
+            var doc = getRangeDocument(this);
+            var el = doc.createElement("body");
+            el.innerHTML = fragmentStr;
+
+            return dom.fragmentFromNodeChildren(el);
+        };
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
+        "commonAncestorContainer"];
+
+    var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
+    var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
+
+    function RangePrototype() {}
+
+    RangePrototype.prototype = {
+        attachListener: function(type, listener) {
+            this._listeners[type].push(listener);
+        },
+
+        compareBoundaryPoints: function(how, range) {
+            assertRangeValid(this);
+            assertSameDocumentOrFragment(this.startContainer, range.startContainer);
+
+            var nodeA, offsetA, nodeB, offsetB;
+            var prefixA = (how == e2s || how == s2s) ? "start" : "end";
+            var prefixB = (how == s2e || how == s2s) ? "start" : "end";
+            nodeA = this[prefixA + "Container"];
+            offsetA = this[prefixA + "Offset"];
+            nodeB = range[prefixB + "Container"];
+            offsetB = range[prefixB + "Offset"];
+            return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
+        },
+
+        insertNode: function(node) {
+            assertRangeValid(this);
+            assertValidNodeType(node, insertableNodeTypes);
+            assertNodeNotReadOnly(this.startContainer);
+
+            if (dom.isAncestorOf(node, this.startContainer, true)) {
+                throw new DOMException("HIERARCHY_REQUEST_ERR");
+            }
+
+            // No check for whether the container of the start of the Range is of a type that does not allow
+            // children of the type of node: the browser's DOM implementation should do this for us when we attempt
+            // to add the node
+
+            var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
+            this.setStartBefore(firstNodeInserted);
+        },
+
+        cloneContents: function() {
+            assertRangeValid(this);
+
+            var clone, frag;
+            if (this.collapsed) {
+                return getRangeDocument(this).createDocumentFragment();
+            } else {
+                if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
+                    clone = this.startContainer.cloneNode(true);
+                    clone.data = clone.data.slice(this.startOffset, this.endOffset);
+                    frag = getRangeDocument(this).createDocumentFragment();
+                    frag.appendChild(clone);
+                    return frag;
+                } else {
+                    var iterator = new RangeIterator(this, true);
+                    clone = cloneSubtree(iterator);
+                    iterator.detach();
+                }
+                return clone;
+            }
+        },
+
+        canSurroundContents: function() {
+            assertRangeValid(this);
+            assertNodeNotReadOnly(this.startContainer);
+            assertNodeNotReadOnly(this.endContainer);
+
+            // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
+            // no non-text nodes.
+            var iterator = new RangeIterator(this, true);
+            var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
+                    (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
+            iterator.detach();
+            return !boundariesInvalid;
+        },
+
+        surroundContents: function(node) {
+            assertValidNodeType(node, surroundNodeTypes);
+
+            if (!this.canSurroundContents()) {
+                throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
+            }
+
+            // Extract the contents
+            var content = this.extractContents();
+
+            // Clear the children of the node
+            if (node.hasChildNodes()) {
+                while (node.lastChild) {
+                    node.removeChild(node.lastChild);
+                }
+            }
+
+            // Insert the new node and add the extracted contents
+            insertNodeAtPosition(node, this.startContainer, this.startOffset);
+            node.appendChild(content);
+
+            this.selectNode(node);
+        },
+
+        cloneRange: function() {
+            assertRangeValid(this);
+            var range = new Range(getRangeDocument(this));
+            var i = rangeProperties.length, prop;
+            while (i--) {
+                prop = rangeProperties[i];
+                range[prop] = this[prop];
+            }
+            return range;
+        },
+
+        toString: function() {
+            assertRangeValid(this);
+            var sc = this.startContainer;
+            if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
+                return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
+            } else {
+                var textBits = [], iterator = new RangeIterator(this, true);
+
+                iterateSubtree(iterator, function(node) {
+                    // Accept only text or CDATA nodes, not comments
+
+                    if (node.nodeType == 3 || node.nodeType == 4) {
+                        textBits.push(node.data);
+                    }
+                });
+                iterator.detach();
+                return textBits.join("");
+            }
+        },
+
+        // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
+        // been removed from Mozilla.
+
+        compareNode: function(node) {
+            assertRangeValid(this);
+
+            var parent = node.parentNode;
+            var nodeIndex = dom.getNodeIndex(node);
+
+            if (!parent) {
+                throw new DOMException("NOT_FOUND_ERR");
+            }
+
+            var startComparison = this.comparePoint(parent, nodeIndex),
+                endComparison = this.comparePoint(parent, nodeIndex + 1);
+
+            if (startComparison < 0) { // Node starts before
+                return (endComparison > 0) ? n_b_a : n_b;
+            } else {
+                return (endComparison > 0) ? n_a : n_i;
+            }
+        },
+
+        comparePoint: function(node, offset) {
+            assertRangeValid(this);
+            assertNode(node, "HIERARCHY_REQUEST_ERR");
+            assertSameDocumentOrFragment(node, this.startContainer);
+
+            if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
+                return -1;
+            } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
+                return 1;
+            }
+            return 0;
+        },
+
+        createContextualFragment: createContextualFragment,
+
+        toHtml: function() {
+            assertRangeValid(this);
+            var container = getRangeDocument(this).createElement("div");
+            container.appendChild(this.cloneContents());
+            return container.innerHTML;
+        },
+
+        // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
+        // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
+        intersectsNode: function(node, touchingIsIntersecting) {
+            assertRangeValid(this);
+            assertNode(node, "NOT_FOUND_ERR");
+            if (dom.getDocument(node) !== getRangeDocument(this)) {
+                return false;
+            }
+
+            var parent = node.parentNode, offset = dom.getNodeIndex(node);
+            assertNode(parent, "NOT_FOUND_ERR");
+
+            var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
+                endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
+
+            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
+        },
+
+
+        isPointInRange: function(node, offset) {
+            assertRangeValid(this);
+            assertNode(node, "HIERARCHY_REQUEST_ERR");
+            assertSameDocumentOrFragment(node, this.startContainer);
+
+            return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
+                   (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
+        },
+
+        // The methods below are non-standard and invented by me.
+
+        // Sharing a boundary start-to-end or end-to-start does not count as intersection.
+        intersectsRange: function(range, touchingIsIntersecting) {
+            assertRangeValid(this);
+
+            if (getRangeDocument(range) != getRangeDocument(this)) {
+                throw new DOMException("WRONG_DOCUMENT_ERR");
+            }
+
+            var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
+                endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
+
+            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
+        },
+
+        intersection: function(range) {
+            if (this.intersectsRange(range)) {
+                var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
+                    endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
+
+                var intersectionRange = this.cloneRange();
+
+                if (startComparison == -1) {
+                    intersectionRange.setStart(range.startContainer, range.startOffset);
+                }
+                if (endComparison == 1) {
+                    intersectionRange.setEnd(range.endContainer, range.endOffset);
+                }
+                return intersectionRange;
+            }
+            return null;
+        },
+
+        union: function(range) {
+            if (this.intersectsRange(range, true)) {
+                var unionRange = this.cloneRange();
+                if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
+                    unionRange.setStart(range.startContainer, range.startOffset);
+                }
+                if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
+                    unionRange.setEnd(range.endContainer, range.endOffset);
+                }
+                return unionRange;
+            } else {
+                throw new RangeException("Ranges do not intersect");
+            }
+        },
+
+        containsNode: function(node, allowPartial) {
+            if (allowPartial) {
+                return this.intersectsNode(node, false);
+            } else {
+                return this.compareNode(node) == n_i;
+            }
+        },
+
+        containsNodeContents: function(node) {
+            return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;
+        },
+
+        containsRange: function(range) {
+            return this.intersection(range).equals(range);
+        },
+
+        containsNodeText: function(node) {
+            var nodeRange = this.cloneRange();
+            nodeRange.selectNode(node);
+            var textNodes = nodeRange.getNodes([3]);
+            if (textNodes.length > 0) {
+                nodeRange.setStart(textNodes[0], 0);
+                var lastTextNode = textNodes.pop();
+                nodeRange.setEnd(lastTextNode, lastTextNode.length);
+                var contains = this.containsRange(nodeRange);
+                nodeRange.detach();
+                return contains;
+            } else {
+                return this.containsNodeContents(node);
+            }
+        },
+
+        createNodeIterator: function(nodeTypes, filter) {
+            assertRangeValid(this);
+            return new RangeNodeIterator(this, nodeTypes, filter);
+        },
+
+        getNodes: function(nodeTypes, filter) {
+            assertRangeValid(this);
+            return getNodesInRange(this, nodeTypes, filter);
+        },
+
+        getDocument: function() {
+            return getRangeDocument(this);
+        },
+
+        collapseBefore: function(node) {
+            assertNotDetached(this);
+
+            this.setEndBefore(node);
+            this.collapse(false);
+        },
+
+        collapseAfter: function(node) {
+            assertNotDetached(this);
+
+            this.setStartAfter(node);
+            this.collapse(true);
+        },
+
+        getName: function() {
+            return "DomRange";
+        },
+
+        equals: function(range) {
+            return Range.rangesEqual(this, range);
+        },
+
+        isValid: function() {
+            return isRangeValid(this);
+        },
+
+        inspect: function() {
+            return inspect(this);
+        }
+    };
+
+    function copyComparisonConstantsToObject(obj) {
+        obj.START_TO_START = s2s;
+        obj.START_TO_END = s2e;
+        obj.END_TO_END = e2e;
+        obj.END_TO_START = e2s;
+
+        obj.NODE_BEFORE = n_b;
+        obj.NODE_AFTER = n_a;
+        obj.NODE_BEFORE_AND_AFTER = n_b_a;
+        obj.NODE_INSIDE = n_i;
+    }
+
+    function copyComparisonConstants(constructor) {
+        copyComparisonConstantsToObject(constructor);
+        copyComparisonConstantsToObject(constructor.prototype);
+    }
+
+    function createRangeContentRemover(remover, boundaryUpdater) {
+        return function() {
+            assertRangeValid(this);
+
+            var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
+
+            var iterator = new RangeIterator(this, true);
+
+            // Work out where to position the range after content removal
+            var node, boundary;
+            if (sc !== root) {
+                node = dom.getClosestAncestorIn(sc, root, true);
+                boundary = getBoundaryAfterNode(node);
+                sc = boundary.node;
+                so = boundary.offset;
+            }
+
+            // Check none of the range is read-only
+            iterateSubtree(iterator, assertNodeNotReadOnly);
+
+            iterator.reset();
+
+            // Remove the content
+            var returnValue = remover(iterator);
+            iterator.detach();
+
+            // Move to the new position
+            boundaryUpdater(this, sc, so, sc, so);
+
+            return returnValue;
+        };
+    }
+
+    function createPrototypeRange(constructor, boundaryUpdater, detacher) {
+        function createBeforeAfterNodeSetter(isBefore, isStart) {
+            return function(node) {
+                assertNotDetached(this);
+                assertValidNodeType(node, beforeAfterNodeTypes);
+                assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
+
+                var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
+                (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
+            };
+        }
+
+        function setRangeStart(range, node, offset) {
+            var ec = range.endContainer, eo = range.endOffset;
+            if (node !== range.startContainer || offset !== range.startOffset) {
+                // Check the root containers of the range and the new boundary, and also check whether the new boundary
+                // is after the current end. In either case, collapse the range to the new position
+                if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
+                    ec = node;
+                    eo = offset;
+                }
+                boundaryUpdater(range, node, offset, ec, eo);
+            }
+        }
+
+        function setRangeEnd(range, node, offset) {
+            var sc = range.startContainer, so = range.startOffset;
+            if (node !== range.endContainer || offset !== range.endOffset) {
+                // Check the root containers of the range and the new boundary, and also check whether the new boundary
+                // is after the current end. In either case, collapse the range to the new position
+                if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
+                    sc = node;
+                    so = offset;
+                }
+                boundaryUpdater(range, sc, so, node, offset);
+            }
+        }
+
+        function setRangeStartAndEnd(range, node, offset) {
+            if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
+                boundaryUpdater(range, node, offset, node, offset);
+            }
+        }
+
+        constructor.prototype = new RangePrototype();
+
+        api.util.extend(constructor.prototype, {
+            setStart: function(node, offset) {
+                assertNotDetached(this);
+                assertNoDocTypeNotationEntityAncestor(node, true);
+                assertValidOffset(node, offset);
+
+                setRangeStart(this, node, offset);
+            },
+
+            setEnd: function(node, offset) {
+                assertNotDetached(this);
+                assertNoDocTypeNotationEntityAncestor(node, true);
+                assertValidOffset(node, offset);
+
+                setRangeEnd(this, node, offset);
+            },
+
+            setStartBefore: createBeforeAfterNodeSetter(true, true),
+            setStartAfter: createBeforeAfterNodeSetter(false, true),
+            setEndBefore: createBeforeAfterNodeSetter(true, false),
+            setEndAfter: createBeforeAfterNodeSetter(false, false),
+
+            collapse: function(isStart) {
+                assertRangeValid(this);
+                if (isStart) {
+                    boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
+                } else {
+                    boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
+                }
+            },
+
+            selectNodeContents: function(node) {
+                // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
+                // could be taken to mean only its children. However, browsers implement this the same as selectNode for
+                // text nodes, so I shall do likewise
+                assertNotDetached(this);
+                assertNoDocTypeNotationEntityAncestor(node, true);
+
+                boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));
+            },
+
+            selectNode: function(node) {
+                assertNotDetached(this);
+                assertNoDocTypeNotationEntityAncestor(node, false);
+                assertValidNodeType(node, beforeAfterNodeTypes);
+
+                var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
+                boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
+            },
+
+            extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
+
+            deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
+
+            canSurroundContents: function() {
+                assertRangeValid(this);
+                assertNodeNotReadOnly(this.startContainer);
+                assertNodeNotReadOnly(this.endContainer);
+
+                // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
+                // no non-text nodes.
+                var iterator = new RangeIterator(this, true);
+                var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
+                        (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
+                iterator.detach();
+                return !boundariesInvalid;
+            },
+
+            detach: function() {
+                detacher(this);
+            },
+
+            splitBoundaries: function() {
+                assertRangeValid(this);
+
+
+                var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
+                var startEndSame = (sc === ec);
+
+                if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
+                    dom.splitDataNode(ec, eo);
+
+                }
+
+                if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
+
+                    sc = dom.splitDataNode(sc, so);
+                    if (startEndSame) {
+                        eo -= so;
+                        ec = sc;
+                    } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
+                        eo++;
+                    }
+                    so = 0;
+
+                }
+                boundaryUpdater(this, sc, so, ec, eo);
+            },
+
+            normalizeBoundaries: function() {
+                assertRangeValid(this);
+
+                var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
+
+                var mergeForward = function(node) {
+                    var sibling = node.nextSibling;
+                    if (sibling && sibling.nodeType == node.nodeType) {
+                        ec = node;
+                        eo = node.length;
+                        node.appendData(sibling.data);
+                        sibling.parentNode.removeChild(sibling);
+                    }
+                };
+
+                var mergeBackward = function(node) {
+                    var sibling = node.previousSibling;
+                    if (sibling && sibling.nodeType == node.nodeType) {
+                        sc = node;
+                        var nodeLength = node.length;
+                        so = sibling.length;
+                        node.insertData(0, sibling.data);
+                        sibling.parentNode.removeChild(sibling);
+                        if (sc == ec) {
+                            eo += so;
+                            ec = sc;
+                        } else if (ec == node.parentNode) {
+                            var nodeIndex = dom.getNodeIndex(node);
+                            if (eo == nodeIndex) {
+                                ec = node;
+                                eo = nodeLength;
+                            } else if (eo > nodeIndex) {
+                                eo--;
+                            }
+                        }
+                    }
+                };
+
+                var normalizeStart = true;
+
+                if (dom.isCharacterDataNode(ec)) {
+                    if (ec.length == eo) {
+                        mergeForward(ec);
+                    }
+                } else {
+                    if (eo > 0) {
+                        var endNode = ec.childNodes[eo - 1];
+                        if (endNode && dom.isCharacterDataNode(endNode)) {
+                            mergeForward(endNode);
+                        }
+                    }
+                    normalizeStart = !this.collapsed;
+                }
+
+                if (normalizeStart) {
+                    if (dom.isCharacterDataNode(sc)) {
+                        if (so == 0) {
+                            mergeBackward(sc);
+                        }
+                    } else {
+                        if (so < sc.childNodes.length) {
+                            var startNode = sc.childNodes[so];
+                            if (startNode && dom.isCharacterDataNode(startNode)) {
+                                mergeBackward(startNode);
+                            }
+                        }
+                    }
+                } else {
+                    sc = ec;
+                    so = eo;
+                }
+
+                boundaryUpdater(this, sc, so, ec, eo);
+            },
+
+            collapseToPoint: function(node, offset) {
+                assertNotDetached(this);
+
+                assertNoDocTypeNotationEntityAncestor(node, true);
+                assertValidOffset(node, offset);
+
+                setRangeStartAndEnd(this, node, offset);
+            }
+        });
+
+        copyComparisonConstants(constructor);
+    }
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Updates commonAncestorContainer and collapsed after boundary change
+    function updateCollapsedAndCommonAncestor(range) {
+        range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
+        range.commonAncestorContainer = range.collapsed ?
+            range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
+    }
+
+    function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
+        var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
+        var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
+
+        range.startContainer = startContainer;
+        range.startOffset = startOffset;
+        range.endContainer = endContainer;
+        range.endOffset = endOffset;
+
+        updateCollapsedAndCommonAncestor(range);
+        dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
+    }
+
+    function detach(range) {
+        assertNotDetached(range);
+        range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
+        range.collapsed = range.commonAncestorContainer = null;
+        dispatchEvent(range, "detach", null);
+        range._listeners = null;
+    }
+
+    /**
+     * @constructor
+     */
+    function Range(doc) {
+        this.startContainer = doc;
+        this.startOffset = 0;
+        this.endContainer = doc;
+        this.endOffset = 0;
+        this._listeners = {
+            boundarychange: [],
+            detach: []
+        };
+        updateCollapsedAndCommonAncestor(this);
+    }
+
+    createPrototypeRange(Range, updateBoundaries, detach);
+
+    api.rangePrototype = RangePrototype.prototype;
+
+    Range.rangeProperties = rangeProperties;
+    Range.RangeIterator = RangeIterator;
+    Range.copyComparisonConstants = copyComparisonConstants;
+    Range.createPrototypeRange = createPrototypeRange;
+    Range.inspect = inspect;
+    Range.getRangeDocument = getRangeDocument;
+    Range.rangesEqual = function(r1, r2) {
+        return r1.startContainer === r2.startContainer &&
+               r1.startOffset === r2.startOffset &&
+               r1.endContainer === r2.endContainer &&
+               r1.endOffset === r2.endOffset;
+    };
+
+    api.DomRange = Range;
+    api.RangeException = RangeException;
+});rangy.createModule("WrappedRange", function(api, module) {\r
+    api.requireModules( ["DomUtil", "DomRange"] );\r
+\r
+    /**\r
+     * @constructor\r
+     */\r
+    var WrappedRange;\r
+    var dom = api.dom;\r
+    var DomPosition = dom.DomPosition;\r
+    var DomRange = api.DomRange;\r
+\r
+\r
+\r
+    /*----------------------------------------------------------------------------------------------------------------*/\r
+\r
+    /*\r
+    This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()\r
+    method. For example, in the following (where pipes denote the selection boundaries):\r
+\r
+    <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>\r
+\r
+    var range = document.selection.createRange();\r
+    alert(range.parentElement().id); // Should alert "ul" but alerts "b"\r
+\r
+    This method returns the common ancestor node of the following:\r
+    - the parentElement() of the textRange\r
+    - the parentElement() of the textRange after calling collapse(true)\r
+    - the parentElement() of the textRange after calling collapse(false)\r
+     */\r
+    function getTextRangeContainerElement(textRange) {\r
+        var parentEl = textRange.parentElement();\r
+\r
+        var range = textRange.duplicate();\r
+        range.collapse(true);\r
+        var startEl = range.parentElement();\r
+        range = textRange.duplicate();\r
+        range.collapse(false);\r
+        var endEl = range.parentElement();\r
+        var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);\r
+\r
+        return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);\r
+    }\r
+\r
+    function textRangeIsCollapsed(textRange) {\r
+        return textRange.compareEndPoints("StartToEnd", textRange) == 0;\r
+    }\r
+\r
+    // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as\r
+    // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has\r
+    // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling\r
+    // for inputs and images, plus optimizations.\r
+    function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {\r
+        var workingRange = textRange.duplicate();\r
+\r
+        workingRange.collapse(isStart);\r
+        var containerElement = workingRange.parentElement();\r
+\r
+        // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so\r
+        // check for that\r
+        // TODO: Find out when. Workaround for wholeRangeContainerElement may break this\r
+        if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {\r
+            containerElement = wholeRangeContainerElement;\r
+\r
+        }\r
+\r
+\r
+\r
+        // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and\r
+        // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx\r
+        if (!containerElement.canHaveHTML) {\r
+            return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));\r
+        }\r
+\r
+        var workingNode = dom.getDocument(containerElement).createElement("span");\r
+        var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";\r
+        var previousNode, nextNode, boundaryPosition, boundaryNode;\r
+\r
+        // Move the working range through the container's children, starting at the end and working backwards, until the\r
+        // working range reaches or goes past the boundary we're interested in\r
+        do {\r
+            containerElement.insertBefore(workingNode, workingNode.previousSibling);\r
+            workingRange.moveToElementText(workingNode);\r
+        } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&\r
+                workingNode.previousSibling);\r
+\r
+        // We've now reached or gone past the boundary of the text range we're interested in\r
+        // so have identified the node we want\r
+        boundaryNode = workingNode.nextSibling;\r
+\r
+        if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {\r
+            // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the\r
+            // node containing the text range's boundary, so we move the end of the working range to the boundary point\r
+            // and measure the length of its text to get the boundary's offset within the node.\r
+            workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);\r
+\r
+\r
+            var offset;\r
+\r
+            if (/[\r\n]/.test(boundaryNode.data)) {\r
+                /*\r
+                For the particular case of a boundary within a text node containing line breaks (within a <pre> element,\r
+                for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:\r
+\r
+                - Each line break is represented as \r in the text node's data/nodeValue properties\r
+                - Each line break is represented as \r\n in the TextRange's 'text' property\r
+                - The 'text' property of the TextRange does not contain trailing line breaks\r
+\r
+                To get round the problem presented by the final fact above, we can use the fact that TextRange's\r
+                moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily\r
+                the same as the number of characters it was instructed to move. The simplest approach is to use this to\r
+                store the characters moved when moving both the start and end of the range to the start of the document\r
+                body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).\r
+                However, this is extremely slow when the document is large and the range is near the end of it. Clearly\r
+                doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same\r
+                problem.\r
+\r
+                Another approach that works is to use moveStart() to move the start boundary of the range up to the end\r
+                boundary one character at a time and incrementing a counter with the value returned by the moveStart()\r
+                call. However, the check for whether the start boundary has reached the end boundary is expensive, so\r
+                this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of\r
+                the range within the document).\r
+\r
+                The method below is a hybrid of the two methods above. It uses the fact that a string containing the\r
+                TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the\r
+                text of the TextRange, so the start of the range is moved that length initially and then a character at\r
+                a time to make up for any trailing line breaks not contained in the 'text' property. This has good\r
+                performance in most situations compared to the previous two methods.\r
+                */\r
+                var tempRange = workingRange.duplicate();\r
+                var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;\r
+\r
+                offset = tempRange.moveStart("character", rangeLength);\r
+                while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {\r
+                    offset++;\r
+                    tempRange.moveStart("character", 1);\r
+                }\r
+            } else {\r
+                offset = workingRange.text.length;\r
+            }\r
+            boundaryPosition = new DomPosition(boundaryNode, offset);\r
+        } else {\r
+\r
+\r
+            // If the boundary immediately follows a character data node and this is the end boundary, we should favour\r
+            // a position within that, and likewise for a start boundary preceding a character data node\r
+            previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;\r
+            nextNode = (isCollapsed || isStart) && workingNode.nextSibling;\r
+\r
+\r
+\r
+            if (nextNode && dom.isCharacterDataNode(nextNode)) {\r
+                boundaryPosition = new DomPosition(nextNode, 0);\r
+            } else if (previousNode && dom.isCharacterDataNode(previousNode)) {\r
+                boundaryPosition = new DomPosition(previousNode, previousNode.length);\r
+            } else {\r
+                boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));\r
+            }\r
+        }\r
+\r
+        // Clean up\r
+        workingNode.parentNode.removeChild(workingNode);\r
+\r
+        return boundaryPosition;\r
+    }\r
+\r
+    // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.\r
+    // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange\r
+    // (http://code.google.com/p/ierange/)\r
+    function createBoundaryTextRange(boundaryPosition, isStart) {\r
+        var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;\r
+        var doc = dom.getDocument(boundaryPosition.node);\r
+        var workingNode, childNodes, workingRange = doc.body.createTextRange();\r
+        var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);\r
+\r
+        if (nodeIsDataNode) {\r
+            boundaryNode = boundaryPosition.node;\r
+            boundaryParent = boundaryNode.parentNode;\r
+        } else {\r
+            childNodes = boundaryPosition.node.childNodes;\r
+            boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;\r
+            boundaryParent = boundaryPosition.node;\r
+        }\r
+\r
+        // Position the range immediately before the node containing the boundary\r
+        workingNode = doc.createElement("span");\r
+\r
+        // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the\r
+        // element rather than immediately before or after it, which is what we want\r
+        workingNode.innerHTML = "&#feff;";\r
+\r
+        // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report\r
+        // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12\r
+        if (boundaryNode) {\r
+            boundaryParent.insertBefore(workingNode, boundaryNode);\r
+        } else {\r
+            boundaryParent.appendChild(workingNode);\r
+        }\r
+\r
+        workingRange.moveToElementText(workingNode);\r
+        workingRange.collapse(!isStart);\r
+\r
+        // Clean up\r
+        boundaryParent.removeChild(workingNode);\r
+\r
+        // Move the working range to the text offset, if required\r
+        if (nodeIsDataNode) {\r
+            workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);\r
+        }\r
+\r
+        return workingRange;\r
+    }\r
+\r
+    /*----------------------------------------------------------------------------------------------------------------*/\r
+\r
+    if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {\r
+        // This is a wrapper around the browser's native DOM Range. It has two aims:\r
+        // - Provide workarounds for specific browser bugs\r
+        // - provide convenient extensions, which are inherited from Rangy's DomRange\r
+\r
+        (function() {\r
+            var rangeProto;\r
+            var rangeProperties = DomRange.rangeProperties;\r
+            var canSetRangeStartAfterEnd;\r
+\r
+            function updateRangeProperties(range) {\r
+                var i = rangeProperties.length, prop;\r
+                while (i--) {\r
+                    prop = rangeProperties[i];\r
+                    range[prop] = range.nativeRange[prop];\r
+                }\r
+            }\r
+\r
+            function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {\r
+                var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);\r
+                var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);\r
+\r
+                // Always set both boundaries for the benefit of IE9 (see issue 35)\r
+                if (startMoved || endMoved) {\r
+                    range.setEnd(endContainer, endOffset);\r
+                    range.setStart(startContainer, startOffset);\r
+                }\r
+            }\r
+\r
+            function detach(range) {\r
+                range.nativeRange.detach();\r
+                range.detached = true;\r
+                var i = rangeProperties.length, prop;\r
+                while (i--) {\r
+                    prop = rangeProperties[i];\r
+                    range[prop] = null;\r
+                }\r
+            }\r
+\r
+            var createBeforeAfterNodeSetter;\r
+\r
+            WrappedRange = function(range) {\r
+                if (!range) {\r
+                    throw new Error("Range must be specified");\r
+                }\r
+                this.nativeRange = range;\r
+                updateRangeProperties(this);\r
+            };\r
+\r
+            DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);\r
+\r
+            rangeProto = WrappedRange.prototype;\r
+\r
+            rangeProto.selectNode = function(node) {\r
+                this.nativeRange.selectNode(node);\r
+                updateRangeProperties(this);\r
+            };\r
+\r
+            rangeProto.deleteContents = function() {\r
+                this.nativeRange.deleteContents();\r
+                updateRangeProperties(this);\r
+            };\r
+\r
+            rangeProto.extractContents = function() {\r
+                var frag = this.nativeRange.extractContents();\r
+                updateRangeProperties(this);\r
+                return frag;\r
+            };\r
+\r
+            rangeProto.cloneContents = function() {\r
+                return this.nativeRange.cloneContents();\r
+            };\r
+\r
+            // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still\r
+            // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for\r
+            // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of\r
+            // insertNode, which works but is almost certainly slower than the native implementation.\r
+/*\r
+            rangeProto.insertNode = function(node) {\r
+                this.nativeRange.insertNode(node);\r
+                updateRangeProperties(this);\r
+            };\r
+*/\r
+\r
+            rangeProto.surroundContents = function(node) {\r
+                this.nativeRange.surroundContents(node);\r
+                updateRangeProperties(this);\r
+            };\r
+\r
+            rangeProto.collapse = function(isStart) {\r
+                this.nativeRange.collapse(isStart);\r
+                updateRangeProperties(this);\r
+            };\r
+\r
+            rangeProto.cloneRange = function() {\r
+                return new WrappedRange(this.nativeRange.cloneRange());\r
+            };\r
+\r
+            rangeProto.refresh = function() {\r
+                updateRangeProperties(this);\r
+            };\r
+\r
+            rangeProto.toString = function() {\r
+                return this.nativeRange.toString();\r
+            };\r
+\r
+            // Create test range and node for feature detection\r
+\r
+            var testTextNode = document.createTextNode("test");\r
+            dom.getBody(document).appendChild(testTextNode);\r
+            var range = document.createRange();\r
+\r
+            /*--------------------------------------------------------------------------------------------------------*/\r
+\r
+            // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and\r
+            // correct for it\r
+\r
+            range.setStart(testTextNode, 0);\r
+            range.setEnd(testTextNode, 0);\r
+\r
+            try {\r
+                range.setStart(testTextNode, 1);\r
+                canSetRangeStartAfterEnd = true;\r
+\r
+                rangeProto.setStart = function(node, offset) {\r
+                    this.nativeRange.setStart(node, offset);\r
+                    updateRangeProperties(this);\r
+                };\r
+\r
+                rangeProto.setEnd = function(node, offset) {\r
+                    this.nativeRange.setEnd(node, offset);\r
+                    updateRangeProperties(this);\r
+                };\r
+\r
+                createBeforeAfterNodeSetter = function(name) {\r
+                    return function(node) {\r
+                        this.nativeRange[name](node);\r
+                        updateRangeProperties(this);\r
+                    };\r
+                };\r
+\r
+            } catch(ex) {\r
+\r
+\r
+                canSetRangeStartAfterEnd = false;\r
+\r
+                rangeProto.setStart = function(node, offset) {\r
+                    try {\r
+                        this.nativeRange.setStart(node, offset);\r
+                    } catch (ex) {\r
+                        this.nativeRange.setEnd(node, offset);\r
+                        this.nativeRange.setStart(node, offset);\r
+                    }\r
+                    updateRangeProperties(this);\r
+                };\r
+\r
+                rangeProto.setEnd = function(node, offset) {\r
+                    try {\r
+                        this.nativeRange.setEnd(node, offset);\r
+                    } catch (ex) {\r
+                        this.nativeRange.setStart(node, offset);\r
+                        this.nativeRange.setEnd(node, offset);\r
+                    }\r
+                    updateRangeProperties(this);\r
+                };\r
+\r
+                createBeforeAfterNodeSetter = function(name, oppositeName) {\r
+                    return function(node) {\r
+                        try {\r
+                            this.nativeRange[name](node);\r
+                        } catch (ex) {\r
+                            this.nativeRange[oppositeName](node);\r
+                            this.nativeRange[name](node);\r
+                        }\r
+                        updateRangeProperties(this);\r
+                    };\r
+                };\r
+            }\r
+\r
+            rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");\r
+            rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");\r
+            rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");\r
+            rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");\r
+\r
+            /*--------------------------------------------------------------------------------------------------------*/\r
+\r
+            // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to\r
+            // the 0th character of the text node\r
+            range.selectNodeContents(testTextNode);\r
+            if (range.startContainer == testTextNode && range.endContainer == testTextNode &&\r
+                    range.startOffset == 0 && range.endOffset == testTextNode.length) {\r
+                rangeProto.selectNodeContents = function(node) {\r
+                    this.nativeRange.selectNodeContents(node);\r
+                    updateRangeProperties(this);\r
+                };\r
+            } else {\r
+                rangeProto.selectNodeContents = function(node) {\r
+                    this.setStart(node, 0);\r
+                    this.setEnd(node, DomRange.getEndOffset(node));\r
+                };\r
+            }\r
+\r
+            /*--------------------------------------------------------------------------------------------------------*/\r
+\r
+            // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants\r
+            // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738\r
+\r
+            range.selectNodeContents(testTextNode);\r
+            range.setEnd(testTextNode, 3);\r
+\r
+            var range2 = document.createRange();\r
+            range2.selectNodeContents(testTextNode);\r
+            range2.setEnd(testTextNode, 4);\r
+            range2.setStart(testTextNode, 2);\r
+\r
+            if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &\r
+                    range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {\r
+                // This is the wrong way round, so correct for it\r
+\r
+\r
+                rangeProto.compareBoundaryPoints = function(type, range) {\r
+                    range = range.nativeRange || range;\r
+                    if (type == range.START_TO_END) {\r
+                        type = range.END_TO_START;\r
+                    } else if (type == range.END_TO_START) {\r
+                        type = range.START_TO_END;\r
+                    }\r
+                    return this.nativeRange.compareBoundaryPoints(type, range);\r
+                };\r
+            } else {\r
+                rangeProto.compareBoundaryPoints = function(type, range) {\r
+                    return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);\r
+                };\r
+            }\r
+\r
+            /*--------------------------------------------------------------------------------------------------------*/\r
+\r
+            // Test for existence of createContextualFragment and delegate to it if it exists\r
+            if (api.util.isHostMethod(range, "createContextualFragment")) {\r
+                rangeProto.createContextualFragment = function(fragmentStr) {\r
+                    return this.nativeRange.createContextualFragment(fragmentStr);\r
+                };\r
+            }\r
+\r
+            /*--------------------------------------------------------------------------------------------------------*/\r
+\r
+            // Clean up\r
+            dom.getBody(document).removeChild(testTextNode);\r
+            range.detach();\r
+            range2.detach();\r
+        })();\r
+\r
+        api.createNativeRange = function(doc) {\r
+            doc = doc || document;\r
+            return doc.createRange();\r
+        };\r
+    } else if (api.features.implementsTextRange) {\r
+        // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a\r
+        // prototype\r
+\r
+        WrappedRange = function(textRange) {\r
+            this.textRange = textRange;\r
+            this.refresh();\r
+        };\r
+\r
+        WrappedRange.prototype = new DomRange(document);\r
+\r
+        WrappedRange.prototype.refresh = function() {\r
+            var start, end;\r
+\r
+            // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.\r
+            var rangeContainerElement = getTextRangeContainerElement(this.textRange);\r
+\r
+            if (textRangeIsCollapsed(this.textRange)) {\r
+                end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);\r
+            } else {\r
+\r
+                start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);\r
+                end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);\r
+            }\r
+\r
+            this.setStart(start.node, start.offset);\r
+            this.setEnd(end.node, end.offset);\r
+        };\r
+\r
+        DomRange.copyComparisonConstants(WrappedRange);\r
+\r
+        // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work\r
+        var globalObj = (function() { return this; })();\r
+        if (typeof globalObj.Range == "undefined") {\r
+            globalObj.Range = WrappedRange;\r
+        }\r
+\r
+        api.createNativeRange = function(doc) {\r
+            doc = doc || document;\r
+            return doc.body.createTextRange();\r
+        };\r
+    }\r
+\r
+    if (api.features.implementsTextRange) {\r
+        WrappedRange.rangeToTextRange = function(range) {\r
+            if (range.collapsed) {\r
+                var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);\r
+\r
+\r
+\r
+                return tr;\r
+\r
+                //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);\r
+            } else {\r
+                var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);\r
+                var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);\r
+                var textRange = dom.getDocument(range.startContainer).body.createTextRange();\r
+                textRange.setEndPoint("StartToStart", startRange);\r
+                textRange.setEndPoint("EndToEnd", endRange);\r
+                return textRange;\r
+            }\r
+        };\r
+    }\r
+\r
+    WrappedRange.prototype.getName = function() {\r
+        return "WrappedRange";\r
+    };\r
+\r
+    api.WrappedRange = WrappedRange;\r
+\r
+    api.createRange = function(doc) {\r
+        doc = doc || document;\r
+        return new WrappedRange(api.createNativeRange(doc));\r
+    };\r
+\r
+    api.createRangyRange = function(doc) {\r
+        doc = doc || document;\r
+        return new DomRange(doc);\r
+    };\r
+\r
+    api.createIframeRange = function(iframeEl) {\r
+        return api.createRange(dom.getIframeDocument(iframeEl));\r
+    };\r
+\r
+    api.createIframeRangyRange = function(iframeEl) {\r
+        return api.createRangyRange(dom.getIframeDocument(iframeEl));\r
+    };\r
+\r
+    api.addCreateMissingNativeApiListener(function(win) {\r
+        var doc = win.document;\r
+        if (typeof doc.createRange == "undefined") {\r
+            doc.createRange = function() {\r
+                return api.createRange(this);\r
+            };\r
+        }\r
+        doc = win = null;\r
+    });\r
+});rangy.createModule("WrappedSelection", function(api, module) {\r
+    // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range\r
+    // spec (http://html5.org/specs/dom-range.html)\r
+\r
+    api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );\r
+\r
+    api.config.checkSelectionRanges = true;\r
+\r
+    var BOOLEAN = "boolean",\r
+        windowPropertyName = "_rangySelection",\r
+        dom = api.dom,\r
+        util = api.util,\r
+        DomRange = api.DomRange,\r
+        WrappedRange = api.WrappedRange,\r
+        DOMException = api.DOMException,\r
+        DomPosition = dom.DomPosition,\r
+        getSelection,\r
+        selectionIsCollapsed,\r
+        CONTROL = "Control";\r
+\r
+\r
+\r
+    function getWinSelection(winParam) {\r
+        return (winParam || window).getSelection();\r
+    }\r
+\r
+    function getDocSelection(winParam) {\r
+        return (winParam || window).document.selection;\r
+    }\r
+\r
+    // Test for the Range/TextRange and Selection features required\r
+    // Test for ability to retrieve selection\r
+    var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),\r
+        implementsDocSelection = api.util.isHostObject(document, "selection");\r
+\r
+    var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);\r
+\r
+    if (useDocumentSelection) {\r
+        getSelection = getDocSelection;\r
+        api.isSelectionValid = function(winParam) {\r
+            var doc = (winParam || window).document, nativeSel = doc.selection;\r
+\r
+            // Check whether the selection TextRange is actually contained within the correct document\r
+            return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);\r
+        };\r
+    } else if (implementsWinGetSelection) {\r
+        getSelection = getWinSelection;\r
+        api.isSelectionValid = function() {\r
+            return true;\r
+        };\r
+    } else {\r
+        module.fail("Neither document.selection or window.getSelection() detected.");\r
+    }\r
+\r
+    api.getNativeSelection = getSelection;\r
+\r
+    var testSelection = getSelection();\r
+    var testRange = api.createNativeRange(document);\r
+    var body = dom.getBody(document);\r
+\r
+    // Obtaining a range from a selection\r
+    var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&\r
+                                     util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));\r
+    api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;\r
+\r
+    // Test for existence of native selection extend() method\r
+    var selectionHasExtend = util.isHostMethod(testSelection, "extend");\r
+    api.features.selectionHasExtend = selectionHasExtend;\r
+\r
+    // Test if rangeCount exists\r
+    var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");\r
+    api.features.selectionHasRangeCount = selectionHasRangeCount;\r
+\r
+    var selectionSupportsMultipleRanges = false;\r
+    var collapsedNonEditableSelectionsSupported = true;\r
+\r
+    if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&\r
+            typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {\r
+\r
+        (function() {\r
+            var iframe = document.createElement("iframe");\r
+            iframe.frameBorder = 0;\r
+            iframe.style.position = "absolute";\r
+            iframe.style.left = "-10000px";\r
+            body.appendChild(iframe);\r
+\r
+            var iframeDoc = dom.getIframeDocument(iframe);\r
+            iframeDoc.open();\r
+            iframeDoc.write("<html><head></head><body>12</body></html>");\r
+            iframeDoc.close();\r
+\r
+            var sel = dom.getIframeWindow(iframe).getSelection();\r
+            var docEl = iframeDoc.documentElement;\r
+            var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;\r
+\r
+            // Test whether the native selection will allow a collapsed selection within a non-editable element\r
+            var r1 = iframeDoc.createRange();\r
+            r1.setStart(textNode, 1);\r
+            r1.collapse(true);\r
+            sel.addRange(r1);\r
+            collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);\r
+            sel.removeAllRanges();\r
+\r
+            // Test whether the native selection is capable of supporting multiple ranges\r
+            var r2 = r1.cloneRange();\r
+            r1.setStart(textNode, 0);\r
+            r2.setEnd(textNode, 2);\r
+            sel.addRange(r1);\r
+            sel.addRange(r2);\r
+\r
+            selectionSupportsMultipleRanges = (sel.rangeCount == 2);\r
+\r
+            // Clean up\r
+            r1.detach();\r
+            r2.detach();\r
+\r
+            body.removeChild(iframe);\r
+        })();\r
+    }\r
+\r
+    api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;\r
+    api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;\r
+\r
+    // ControlRanges\r
+    var implementsControlRange = false, testControlRange;\r
+\r
+    if (body && util.isHostMethod(body, "createControlRange")) {\r
+        testControlRange = body.createControlRange();\r
+        if (util.areHostProperties(testControlRange, ["item", "add"])) {\r
+            implementsControlRange = true;\r
+        }\r
+    }\r
+    api.features.implementsControlRange = implementsControlRange;\r
+\r
+    // Selection collapsedness\r
+    if (selectionHasAnchorAndFocus) {\r
+        selectionIsCollapsed = function(sel) {\r
+            return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;\r
+        };\r
+    } else {\r
+        selectionIsCollapsed = function(sel) {\r
+            return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;\r
+        };\r
+    }\r
+\r
+    function updateAnchorAndFocusFromRange(sel, range, backwards) {\r
+        var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";\r
+        sel.anchorNode = range[anchorPrefix + "Container"];\r
+        sel.anchorOffset = range[anchorPrefix + "Offset"];\r
+        sel.focusNode = range[focusPrefix + "Container"];\r
+        sel.focusOffset = range[focusPrefix + "Offset"];\r
+    }\r
+\r
+    function updateAnchorAndFocusFromNativeSelection(sel) {\r
+        var nativeSel = sel.nativeSelection;\r
+        sel.anchorNode = nativeSel.anchorNode;\r
+        sel.anchorOffset = nativeSel.anchorOffset;\r
+        sel.focusNode = nativeSel.focusNode;\r
+        sel.focusOffset = nativeSel.focusOffset;\r
+    }\r
+\r
+    function updateEmptySelection(sel) {\r
+        sel.anchorNode = sel.focusNode = null;\r
+        sel.anchorOffset = sel.focusOffset = 0;\r
+        sel.rangeCount = 0;\r
+        sel.isCollapsed = true;\r
+        sel._ranges.length = 0;\r
+    }\r
+\r
+    function getNativeRange(range) {\r
+        var nativeRange;\r
+        if (range instanceof DomRange) {\r
+            nativeRange = range._selectionNativeRange;\r
+            if (!nativeRange) {\r
+                nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));\r
+                nativeRange.setEnd(range.endContainer, range.endOffset);\r
+                nativeRange.setStart(range.startContainer, range.startOffset);\r
+                range._selectionNativeRange = nativeRange;\r
+                range.attachListener("detach", function() {\r
+\r
+                    this._selectionNativeRange = null;\r
+                });\r
+            }\r
+        } else if (range instanceof WrappedRange) {\r
+            nativeRange = range.nativeRange;\r
+        } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {\r
+            nativeRange = range;\r
+        }\r
+        return nativeRange;\r
+    }\r
+\r
+    function rangeContainsSingleElement(rangeNodes) {\r
+        if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {\r
+            return false;\r
+        }\r
+        for (var i = 1, len = rangeNodes.length; i < len; ++i) {\r
+            if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {\r
+                return false;\r
+            }\r
+        }\r
+        return true;\r
+    }\r
+\r
+    function getSingleElementFromRange(range) {\r
+        var nodes = range.getNodes();\r
+        if (!rangeContainsSingleElement(nodes)) {\r
+            throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");\r
+        }\r
+        return nodes[0];\r
+    }\r
+\r
+    function isTextRange(range) {\r
+        return !!range && typeof range.text != "undefined";\r
+    }\r
+\r
+    function updateFromTextRange(sel, range) {\r
+        // Create a Range from the selected TextRange\r
+        var wrappedRange = new WrappedRange(range);\r
+        sel._ranges = [wrappedRange];\r
+\r
+        updateAnchorAndFocusFromRange(sel, wrappedRange, false);\r
+        sel.rangeCount = 1;\r
+        sel.isCollapsed = wrappedRange.collapsed;\r
+    }\r
+\r
+    function updateControlSelection(sel) {\r
+        // Update the wrapped selection based on what's now in the native selection\r
+        sel._ranges.length = 0;\r
+        if (sel.docSelection.type == "None") {\r
+            updateEmptySelection(sel);\r
+        } else {\r
+            var controlRange = sel.docSelection.createRange();\r
+            if (isTextRange(controlRange)) {\r
+                // This case (where the selection type is "Control" and calling createRange() on the selection returns\r
+                // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected\r
+                // ControlRange have been removed from the ControlRange and removed from the document.\r
+                updateFromTextRange(sel, controlRange);\r
+            } else {\r
+                sel.rangeCount = controlRange.length;\r
+                var range, doc = dom.getDocument(controlRange.item(0));\r
+                for (var i = 0; i < sel.rangeCount; ++i) {\r
+                    range = api.createRange(doc);\r
+                    range.selectNode(controlRange.item(i));\r
+                    sel._ranges.push(range);\r
+                }\r
+                sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;\r
+                updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);\r
+            }\r
+        }\r
+    }\r
+\r
+    function addRangeToControlSelection(sel, range) {\r
+        var controlRange = sel.docSelection.createRange();\r
+        var rangeElement = getSingleElementFromRange(range);\r
+\r
+        // Create a new ControlRange containing all the elements in the selected ControlRange plus the element\r
+        // contained by the supplied range\r
+        var doc = dom.getDocument(controlRange.item(0));\r
+        var newControlRange = dom.getBody(doc).createControlRange();\r
+        for (var i = 0, len = controlRange.length; i < len; ++i) {\r
+            newControlRange.add(controlRange.item(i));\r
+        }\r
+        try {\r
+            newControlRange.add(rangeElement);\r
+        } catch (ex) {\r
+            throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");\r
+        }\r
+        newControlRange.select();\r
+\r
+        // Update the wrapped selection based on what's now in the native selection\r
+        updateControlSelection(sel);\r
+    }\r
+\r
+    var getSelectionRangeAt;\r
+\r
+    if (util.isHostMethod(testSelection,  "getRangeAt")) {\r
+        getSelectionRangeAt = function(sel, index) {\r
+            try {\r
+                return sel.getRangeAt(index);\r
+            } catch(ex) {\r
+                return null;\r
+            }\r
+        };\r
+    } else if (selectionHasAnchorAndFocus) {\r
+        getSelectionRangeAt = function(sel) {\r
+            var doc = dom.getDocument(sel.anchorNode);\r
+            var range = api.createRange(doc);\r
+            range.setStart(sel.anchorNode, sel.anchorOffset);\r
+            range.setEnd(sel.focusNode, sel.focusOffset);\r
+\r
+            // Handle the case when the selection was selected backwards (from the end to the start in the\r
+            // document)\r
+            if (range.collapsed !== this.isCollapsed) {\r
+                range.setStart(sel.focusNode, sel.focusOffset);\r
+                range.setEnd(sel.anchorNode, sel.anchorOffset);\r
+            }\r
+\r
+            return range;\r
+        };\r
+    }\r
+\r
+    /**\r
+     * @constructor\r
+     */\r
+    function WrappedSelection(selection, docSelection, win) {\r
+        this.nativeSelection = selection;\r
+        this.docSelection = docSelection;\r
+        this._ranges = [];\r
+        this.win = win;\r
+        this.refresh();\r
+    }\r
+\r
+    api.getSelection = function(win) {\r
+        win = win || window;\r
+        var sel = win[windowPropertyName];\r
+        var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;\r
+        if (sel) {\r
+            sel.nativeSelection = nativeSel;\r
+            sel.docSelection = docSel;\r
+            sel.refresh(win);\r
+        } else {\r
+            sel = new WrappedSelection(nativeSel, docSel, win);\r
+            win[windowPropertyName] = sel;\r
+        }\r
+        return sel;\r
+    };\r
+\r
+    api.getIframeSelection = function(iframeEl) {\r
+        return api.getSelection(dom.getIframeWindow(iframeEl));\r
+    };\r
+\r
+    var selProto = WrappedSelection.prototype;\r
+\r
+    function createControlSelection(sel, ranges) {\r
+        // Ensure that the selection becomes of type "Control"\r
+        var doc = dom.getDocument(ranges[0].startContainer);\r
+        var controlRange = dom.getBody(doc).createControlRange();\r
+        for (var i = 0, el; i < rangeCount; ++i) {\r
+            el = getSingleElementFromRange(ranges[i]);\r
+            try {\r
+                controlRange.add(el);\r
+            } catch (ex) {\r
+                throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");\r
+            }\r
+        }\r
+        controlRange.select();\r
+\r
+        // Update the wrapped selection based on what's now in the native selection\r
+        updateControlSelection(sel);\r
+    }\r
+\r
+    // Selecting a range\r
+    if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {\r
+        selProto.removeAllRanges = function() {\r
+            this.nativeSelection.removeAllRanges();\r
+            updateEmptySelection(this);\r
+        };\r
+\r
+        var addRangeBackwards = function(sel, range) {\r
+            var doc = DomRange.getRangeDocument(range);\r
+            var endRange = api.createRange(doc);\r
+            endRange.collapseToPoint(range.endContainer, range.endOffset);\r
+            sel.nativeSelection.addRange(getNativeRange(endRange));\r
+            sel.nativeSelection.extend(range.startContainer, range.startOffset);\r
+            sel.refresh();\r
+        };\r
+\r
+        if (selectionHasRangeCount) {\r
+            selProto.addRange = function(range, backwards) {\r
+                if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {\r
+                    addRangeToControlSelection(this, range);\r
+                } else {\r
+                    if (backwards && selectionHasExtend) {\r
+                        addRangeBackwards(this, range);\r
+                    } else {\r
+                        var previousRangeCount;\r
+                        if (selectionSupportsMultipleRanges) {\r
+                            previousRangeCount = this.rangeCount;\r
+                        } else {\r
+                            this.removeAllRanges();\r
+                            previousRangeCount = 0;\r
+                        }\r
+                        this.nativeSelection.addRange(getNativeRange(range));\r
+\r
+                        // Check whether adding the range was successful\r
+                        this.rangeCount = this.nativeSelection.rangeCount;\r
+\r
+                        if (this.rangeCount == previousRangeCount + 1) {\r
+                            // The range was added successfully\r
+\r
+                            // Check whether the range that we added to the selection is reflected in the last range extracted from\r
+                            // the selection\r
+                            if (api.config.checkSelectionRanges) {\r
+                                var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);\r
+                                if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {\r
+                                    // Happens in WebKit with, for example, a selection placed at the start of a text node\r
+                                    range = new WrappedRange(nativeRange);\r
+                                }\r
+                            }\r
+                            this._ranges[this.rangeCount - 1] = range;\r
+                            updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));\r
+                            this.isCollapsed = selectionIsCollapsed(this);\r
+                        } else {\r
+                            // The range was not added successfully. The simplest thing is to refresh\r
+                            this.refresh();\r
+                        }\r
+                    }\r
+                }\r
+            };\r
+        } else {\r
+            selProto.addRange = function(range, backwards) {\r
+                if (backwards && selectionHasExtend) {\r
+                    addRangeBackwards(this, range);\r
+                } else {\r
+                    this.nativeSelection.addRange(getNativeRange(range));\r
+                    this.refresh();\r
+                }\r
+            };\r
+        }\r
+\r
+        selProto.setRanges = function(ranges) {\r
+            if (implementsControlRange && ranges.length > 1) {\r
+                createControlSelection(this, ranges);\r
+            } else {\r
+                this.removeAllRanges();\r
+                for (var i = 0, len = ranges.length; i < len; ++i) {\r
+                    this.addRange(ranges[i]);\r
+                }\r
+            }\r
+        };\r
+    } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&\r
+               implementsControlRange && useDocumentSelection) {\r
+\r
+        selProto.removeAllRanges = function() {\r
+            // Added try/catch as fix for issue #21\r
+            try {\r
+                this.docSelection.empty();\r
+\r
+                // Check for empty() not working (issue #24)\r
+                if (this.docSelection.type != "None") {\r
+                    // Work around failure to empty a control selection by instead selecting a TextRange and then\r
+                    // calling empty()\r
+                    var doc;\r
+                    if (this.anchorNode) {\r
+                        doc = dom.getDocument(this.anchorNode);\r
+                    } else if (this.docSelection.type == CONTROL) {\r
+                        var controlRange = this.docSelection.createRange();\r
+                        if (controlRange.length) {\r
+                            doc = dom.getDocument(controlRange.item(0)).body.createTextRange();\r
+                        }\r
+                    }\r
+                    if (doc) {\r
+                        var textRange = doc.body.createTextRange();\r
+                        textRange.select();\r
+                        this.docSelection.empty();\r
+                    }\r
+                }\r
+            } catch(ex) {}\r
+            updateEmptySelection(this);\r
+        };\r
+\r
+        selProto.addRange = function(range) {\r
+            if (this.docSelection.type == CONTROL) {\r
+                addRangeToControlSelection(this, range);\r
+            } else {\r
+                WrappedRange.rangeToTextRange(range).select();\r
+                this._ranges[0] = range;\r
+                this.rangeCount = 1;\r
+                this.isCollapsed = this._ranges[0].collapsed;\r
+                updateAnchorAndFocusFromRange(this, range, false);\r
+            }\r
+        };\r
+\r
+        selProto.setRanges = function(ranges) {\r
+            this.removeAllRanges();\r
+            var rangeCount = ranges.length;\r
+            if (rangeCount > 1) {\r
+                createControlSelection(this, ranges);\r
+            } else if (rangeCount) {\r
+                this.addRange(ranges[0]);\r
+            }\r
+        };\r
+    } else {\r
+        module.fail("No means of selecting a Range or TextRange was found");\r
+        return false;\r
+    }\r
+\r
+    selProto.getRangeAt = function(index) {\r
+        if (index < 0 || index >= this.rangeCount) {\r
+            throw new DOMException("INDEX_SIZE_ERR");\r
+        } else {\r
+            return this._ranges[index];\r
+        }\r
+    };\r
+\r
+    var refreshSelection;\r
+\r
+    if (useDocumentSelection) {\r
+        refreshSelection = function(sel) {\r
+            var range;\r
+            if (api.isSelectionValid(sel.win)) {\r
+                range = sel.docSelection.createRange();\r
+            } else {\r
+                range = dom.getBody(sel.win.document).createTextRange();\r
+                range.collapse(true);\r
+            }\r
+\r
+\r
+            if (sel.docSelection.type == CONTROL) {\r
+                updateControlSelection(sel);\r
+            } else if (isTextRange(range)) {\r
+                updateFromTextRange(sel, range);\r
+            } else {\r
+                updateEmptySelection(sel);\r
+            }\r
+        };\r
+    } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {\r
+        refreshSelection = function(sel) {\r
+            if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {\r
+                updateControlSelection(sel);\r
+            } else {\r
+                sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;\r
+                if (sel.rangeCount) {\r
+                    for (var i = 0, len = sel.rangeCount; i < len; ++i) {\r
+                        sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));\r
+                    }\r
+                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));\r
+                    sel.isCollapsed = selectionIsCollapsed(sel);\r
+                } else {\r
+                    updateEmptySelection(sel);\r
+                }\r
+            }\r
+        };\r
+    } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {\r
+        refreshSelection = function(sel) {\r
+            var range, nativeSel = sel.nativeSelection;\r
+            if (nativeSel.anchorNode) {\r
+                range = getSelectionRangeAt(nativeSel, 0);\r
+                sel._ranges = [range];\r
+                sel.rangeCount = 1;\r
+                updateAnchorAndFocusFromNativeSelection(sel);\r
+                sel.isCollapsed = selectionIsCollapsed(sel);\r
+            } else {\r
+                updateEmptySelection(sel);\r
+            }\r
+        };\r
+    } else {\r
+        module.fail("No means of obtaining a Range or TextRange from the user's selection was found");\r
+        return false;\r
+    }\r
+\r
+    selProto.refresh = function(checkForChanges) {\r
+        var oldRanges = checkForChanges ? this._ranges.slice(0) : null;\r
+        refreshSelection(this);\r
+        if (checkForChanges) {\r
+            var i = oldRanges.length;\r
+            if (i != this._ranges.length) {\r
+                return false;\r
+            }\r
+            while (i--) {\r
+                if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {\r
+                    return false;\r
+                }\r
+            }\r
+            return true;\r
+        }\r
+    };\r
+\r
+    // Removal of a single range\r
+    var removeRangeManually = function(sel, range) {\r
+        var ranges = sel.getAllRanges(), removed = false;\r
+        sel.removeAllRanges();\r
+        for (var i = 0, len = ranges.length; i < len; ++i) {\r
+            if (removed || range !== ranges[i]) {\r
+                sel.addRange(ranges[i]);\r
+            } else {\r
+                // According to the draft WHATWG Range spec, the same range may be added to the selection multiple\r
+                // times. removeRange should only remove the first instance, so the following ensures only the first\r
+                // instance is removed\r
+                removed = true;\r
+            }\r
+        }\r
+        if (!sel.rangeCount) {\r
+            updateEmptySelection(sel);\r
+        }\r
+    };\r
+\r
+    if (implementsControlRange) {\r
+        selProto.removeRange = function(range) {\r
+            if (this.docSelection.type == CONTROL) {\r
+                var controlRange = this.docSelection.createRange();\r
+                var rangeElement = getSingleElementFromRange(range);\r
+\r
+                // Create a new ControlRange containing all the elements in the selected ControlRange minus the\r
+                // element contained by the supplied range\r
+                var doc = dom.getDocument(controlRange.item(0));\r
+                var newControlRange = dom.getBody(doc).createControlRange();\r
+                var el, removed = false;\r
+                for (var i = 0, len = controlRange.length; i < len; ++i) {\r
+                    el = controlRange.item(i);\r
+                    if (el !== rangeElement || removed) {\r
+                        newControlRange.add(controlRange.item(i));\r
+                    } else {\r
+                        removed = true;\r
+                    }\r
+                }\r
+                newControlRange.select();\r
+\r
+                // Update the wrapped selection based on what's now in the native selection\r
+                updateControlSelection(this);\r
+            } else {\r
+                removeRangeManually(this, range);\r
+            }\r
+        };\r
+    } else {\r
+        selProto.removeRange = function(range) {\r
+            removeRangeManually(this, range);\r
+        };\r
+    }\r
+\r
+    // Detecting if a selection is backwards\r
+    var selectionIsBackwards;\r
+    if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {\r
+        selectionIsBackwards = function(sel) {\r
+            var backwards = false;\r
+            if (sel.anchorNode) {\r
+                backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);\r
+            }\r
+            return backwards;\r
+        };\r
+\r
+        selProto.isBackwards = function() {\r
+            return selectionIsBackwards(this);\r
+        };\r
+    } else {\r
+        selectionIsBackwards = selProto.isBackwards = function() {\r
+            return false;\r
+        };\r
+    }\r
+\r
+    // Selection text\r
+    // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation\r
+    selProto.toString = function() {\r
+\r
+        var rangeTexts = [];\r
+        for (var i = 0, len = this.rangeCount; i < len; ++i) {\r
+            rangeTexts[i] = "" + this._ranges[i];\r
+        }\r
+        return rangeTexts.join("");\r
+    };\r
+\r
+    function assertNodeInSameDocument(sel, node) {\r
+        if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {\r
+            throw new DOMException("WRONG_DOCUMENT_ERR");\r
+        }\r
+    }\r
+\r
+    // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used\r
+    selProto.collapse = function(node, offset) {\r
+        assertNodeInSameDocument(this, node);\r
+        var range = api.createRange(dom.getDocument(node));\r
+        range.collapseToPoint(node, offset);\r
+        this.removeAllRanges();\r
+        this.addRange(range);\r
+        this.isCollapsed = true;\r
+    };\r
+\r
+    selProto.collapseToStart = function() {\r
+        if (this.rangeCount) {\r
+            var range = this._ranges[0];\r
+            this.collapse(range.startContainer, range.startOffset);\r
+        } else {\r
+            throw new DOMException("INVALID_STATE_ERR");\r
+        }\r
+    };\r
+\r
+    selProto.collapseToEnd = function() {\r
+        if (this.rangeCount) {\r
+            var range = this._ranges[this.rangeCount - 1];\r
+            this.collapse(range.endContainer, range.endOffset);\r
+        } else {\r
+            throw new DOMException("INVALID_STATE_ERR");\r
+        }\r
+    };\r
+\r
+    // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is\r
+    // never used by Rangy.\r
+    selProto.selectAllChildren = function(node) {\r
+        assertNodeInSameDocument(this, node);\r
+        var range = api.createRange(dom.getDocument(node));\r
+        range.selectNodeContents(node);\r
+        this.removeAllRanges();\r
+        this.addRange(range);\r
+    };\r
+\r
+    selProto.deleteFromDocument = function() {\r
+        // Sepcial behaviour required for Control selections\r
+        if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {\r
+            var controlRange = this.docSelection.createRange();\r
+            var element;\r
+            while (controlRange.length) {\r
+                element = controlRange.item(0);\r
+                controlRange.remove(element);\r
+                element.parentNode.removeChild(element);\r
+            }\r
+            this.refresh();\r
+        } else if (this.rangeCount) {\r
+            var ranges = this.getAllRanges();\r
+            this.removeAllRanges();\r
+            for (var i = 0, len = ranges.length; i < len; ++i) {\r
+                ranges[i].deleteContents();\r
+            }\r
+            // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each\r
+            // range. Firefox moves the selection to where the final selected range was, so we emulate that\r
+            this.addRange(ranges[len - 1]);\r
+        }\r
+    };\r
+\r
+    // The following are non-standard extensions\r
+    selProto.getAllRanges = function() {\r
+        return this._ranges.slice(0);\r
+    };\r
+\r
+    selProto.setSingleRange = function(range) {\r
+        this.setRanges( [range] );\r
+    };\r
+\r
+    selProto.containsNode = function(node, allowPartial) {\r
+        for (var i = 0, len = this._ranges.length; i < len; ++i) {\r
+            if (this._ranges[i].containsNode(node, allowPartial)) {\r
+                return true;\r
+            }\r
+        }\r
+        return false;\r
+    };\r
+\r
+    selProto.toHtml = function() {\r
+        var html = "";\r
+        if (this.rangeCount) {\r
+            var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");\r
+            for (var i = 0, len = this._ranges.length; i < len; ++i) {\r
+                container.appendChild(this._ranges[i].cloneContents());\r
+            }\r
+            html = container.innerHTML;\r
+        }\r
+        return html;\r
+    };\r
+\r
+    function inspect(sel) {\r
+        var rangeInspects = [];\r
+        var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);\r
+        var focus = new DomPosition(sel.focusNode, sel.focusOffset);\r
+        var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";\r
+\r
+        if (typeof sel.rangeCount != "undefined") {\r
+            for (var i = 0, len = sel.rangeCount; i < len; ++i) {\r
+                rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));\r
+            }\r
+        }\r
+        return "[" + name + "(Ranges: " + rangeInspects.join(", ") +\r
+                ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";\r
+\r
+    }\r
+\r
+    selProto.getName = function() {\r
+        return "WrappedSelection";\r
+    };\r
+\r
+    selProto.inspect = function() {\r
+        return inspect(this);\r
+    };\r
+\r
+    selProto.detach = function() {\r
+        this.win[windowPropertyName] = null;\r
+        this.win = this.anchorNode = this.focusNode = null;\r
+    };\r
+\r
+    WrappedSelection.inspect = inspect;\r
+\r
+    api.Selection = WrappedSelection;\r
+\r
+    api.selectionPrototype = selProto;\r
+\r
+    api.addCreateMissingNativeApiListener(function(win) {\r
+        if (typeof win.getSelection == "undefined") {\r
+            win.getSelection = function() {\r
+                return api.getSelection(this);\r
+            };\r
+        }\r
+        win = null;\r
+    });\r
+});\r
diff --git a/Android/webViewMarker/src/main/assets/rangy-serializer.js b/Android/webViewMarker/src/main/assets/rangy-serializer.js
new file mode 100755 (executable)
index 0000000..79edc13
--- /dev/null
@@ -0,0 +1,300 @@
+/**\r
+ * @license Serializer module for Rangy.\r
+ * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a\r
+ * cookie or local storage and restore it on the user's next visit to the same page.\r
+ *\r
+ * Part of Rangy, a cross-browser JavaScript range and selection library\r
+ * http://code.google.com/p/rangy/\r
+ *\r
+ * Depends on Rangy core.\r
+ *\r
+ * Copyright 2012, Tim Down\r
+ * Licensed under the MIT license.\r
+ * Version: 1.2.3\r
+ * Build date: 26 February 2012\r
+ */\r
+rangy.createModule("Serializer", function(api, module) {\r
+    api.requireModules( ["WrappedSelection", "WrappedRange"] );\r
+    var UNDEF = "undefined";\r
+\r
+    // encodeURIComponent and decodeURIComponent are required for cookie handling\r
+    if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {\r
+        module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");\r
+    }\r
+\r
+    // Checksum for checking whether range can be serialized\r
+    var crc32 = (function() {\r
+        function utf8encode(str) {\r
+            var utf8CharCodes = [];\r
+\r
+            for (var i = 0, len = str.length, c; i < len; ++i) {\r
+                c = str.charCodeAt(i);\r
+                if (c < 128) {\r
+                    utf8CharCodes.push(c);\r
+                } else if (c < 2048) {\r
+                    utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);\r
+                } else {\r
+                    utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);\r
+                }\r
+            }\r
+            return utf8CharCodes;\r
+        }\r
+\r
+        var cachedCrcTable = null;\r
+\r
+        function buildCRCTable() {\r
+            var table = [];\r
+            for (var i = 0, j, crc; i < 256; ++i) {\r
+                crc = i;\r
+                j = 8;\r
+                while (j--) {\r
+                    if ((crc & 1) == 1) {\r
+                        crc = (crc >>> 1) ^ 0xEDB88320;\r
+                    } else {\r
+                        crc >>>= 1;\r
+                    }\r
+                }\r
+                table[i] = crc >>> 0;\r
+            }\r
+            return table;\r
+        }\r
+\r
+        function getCrcTable() {\r
+            if (!cachedCrcTable) {\r
+                cachedCrcTable = buildCRCTable();\r
+            }\r
+            return cachedCrcTable;\r
+        }\r
+\r
+        return function(str) {\r
+            var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();\r
+            for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {\r
+                y = (crc ^ utf8CharCodes[i]) & 0xFF;\r
+                crc = (crc >>> 8) ^ crcTable[y];\r
+            }\r
+            return (crc ^ -1) >>> 0;\r
+        };\r
+    })();\r
+\r
+    var dom = api.dom;\r
+\r
+    function escapeTextForHtml(str) {\r
+        return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");\r
+    }\r
+\r
+    function nodeToInfoString(node, infoParts) {\r
+        infoParts = infoParts || [];\r
+        var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;\r
+        var nodeInfo = [nodeType, node.nodeName, childCount].join(":");\r
+        var start = "", end = "";\r
+        switch (nodeType) {\r
+            case 3: // Text node\r
+                start = escapeTextForHtml(node.nodeValue);\r
+                break;\r
+            case 8: // Comment\r
+                start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";\r
+                break;\r
+            default:\r
+                start = "<" + nodeInfo + ">";\r
+                end = "</>";\r
+                break;\r
+        }\r
+        if (start) {\r
+            infoParts.push(start);\r
+        }\r
+        for (var i = 0; i < childCount; ++i) {\r
+            nodeToInfoString(children[i], infoParts);\r
+        }\r
+        if (end) {\r
+            infoParts.push(end);\r
+        }\r
+        return infoParts;\r
+    }\r
+\r
+    // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all\r
+    // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around\r
+    // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's\r
+    // innerHTML whenever the user changes an input within the element.\r
+    function getElementChecksum(el) {\r
+        var info = nodeToInfoString(el).join("");\r
+        return crc32(info).toString(16);\r
+    }\r
+\r
+    function serializePosition(node, offset, rootNode) {\r
+        var pathBits = [], n = node;\r
+        rootNode = rootNode || dom.getDocument(node).documentElement;\r
+        while (n && n != rootNode) {\r
+            pathBits.push(dom.getNodeIndex(n, true));\r
+            n = n.parentNode;\r
+        }\r
+        return pathBits.join("/") + ":" + offset;\r
+    }\r
+\r
+    function deserializePosition(serialized, rootNode, doc) {\r
+        if (rootNode) {\r
+            doc = doc || dom.getDocument(rootNode);\r
+        } else {\r
+            doc = doc || document;\r
+            rootNode = doc.documentElement;\r
+        }\r
+        var bits = serialized.split(":");\r
+        var node = rootNode;\r
+        var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;\r
+\r
+        while (i--) {\r
+            nodeIndex = parseInt(nodeIndices[i], 10);\r
+            if (nodeIndex < node.childNodes.length) {\r
+                node = node.childNodes[parseInt(nodeIndices[i], 10)];\r
+            } else {\r
+                throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +\r
+                        " has no child with index " + nodeIndex + ", " + i);\r
+            }\r
+        }\r
+\r
+        return new dom.DomPosition(node, parseInt(bits[1], 10));\r
+    }\r
+\r
+    function serializeRange(range, omitChecksum, rootNode) {\r
+        rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;\r
+        if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {\r
+            throw new Error("serializeRange: range is not wholly contained within specified root node");\r
+        }\r
+        var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +\r
+            serializePosition(range.endContainer, range.endOffset, rootNode);\r
+        if (!omitChecksum) {\r
+            serialized += "{" + getElementChecksum(rootNode) + "}";\r
+        }\r
+        return serialized;\r
+    }\r
+\r
+    function deserializeRange(serialized, rootNode, doc) {\r
+        if (rootNode) {\r
+            doc = doc || dom.getDocument(rootNode);\r
+        } else {\r
+            doc = doc || document;\r
+            rootNode = doc.documentElement;\r
+        }\r
+        var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);\r
+        var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);\r
+        if (checksum && checksum !== getElementChecksum(rootNode)) {\r
+            throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +\r
+                    ") and target root node (" + rootNodeChecksum + ") do not match");\r
+        }\r
+        var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);\r
+        var range = api.createRange(doc);\r
+        range.setStart(start.node, start.offset);\r
+        range.setEnd(end.node, end.offset);\r
+        return range;\r
+    }\r
+\r
+    function canDeserializeRange(serialized, rootNode, doc) {\r
+        if (rootNode) {\r
+            doc = doc || dom.getDocument(rootNode);\r
+        } else {\r
+            doc = doc || document;\r
+            rootNode = doc.documentElement;\r
+        }\r
+        var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);\r
+        var checksum = result[3];\r
+        return !checksum || checksum === getElementChecksum(rootNode);\r
+    }\r
+\r
+    function serializeSelection(selection, omitChecksum, rootNode) {\r
+        selection = selection || api.getSelection();\r
+        var ranges = selection.getAllRanges(), serializedRanges = [];\r
+        for (var i = 0, len = ranges.length; i < len; ++i) {\r
+            serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);\r
+        }\r
+        return serializedRanges.join("|");\r
+    }\r
+\r
+    function deserializeSelection(serialized, rootNode, win) {\r
+        if (rootNode) {\r
+            win = win || dom.getWindow(rootNode);\r
+        } else {\r
+            win = win || window;\r
+            rootNode = win.document.documentElement;\r
+        }\r
+        var serializedRanges = serialized.split("|");\r
+        var sel = api.getSelection(win);\r
+        var ranges = [];\r
+\r
+        for (var i = 0, len = serializedRanges.length; i < len; ++i) {\r
+            ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);\r
+        }\r
+        sel.setRanges(ranges);\r
+\r
+        return sel;\r
+    }\r
+\r
+    function canDeserializeSelection(serialized, rootNode, win) {\r
+        var doc;\r
+        if (rootNode) {\r
+            doc = win ? win.document : dom.getDocument(rootNode);\r
+        } else {\r
+            win = win || window;\r
+            rootNode = win.document.documentElement;\r
+        }\r
+        var serializedRanges = serialized.split("|");\r
+\r
+        for (var i = 0, len = serializedRanges.length; i < len; ++i) {\r
+            if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {\r
+                return false;\r
+            }\r
+        }\r
+\r
+        return true;\r
+    }\r
+\r
+\r
+    var cookieName = "rangySerializedSelection";\r
+\r
+    function getSerializedSelectionFromCookie(cookie) {\r
+        var parts = cookie.split(/[;,]/);\r
+        for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {\r
+            nameVal = parts[i].split("=");\r
+            if (nameVal[0].replace(/^\s+/, "") == cookieName) {\r
+                val = nameVal[1];\r
+                if (val) {\r
+                    return decodeURIComponent(val.replace(/\s+$/, ""));\r
+                }\r
+            }\r
+        }\r
+        return null;\r
+    }\r
+\r
+    function restoreSelectionFromCookie(win) {\r
+        win = win || window;\r
+        var serialized = getSerializedSelectionFromCookie(win.document.cookie);\r
+        if (serialized) {\r
+            deserializeSelection(serialized, win.doc)\r
+        }\r
+    }\r
+\r
+    function saveSelectionCookie(win, props) {\r
+        win = win || window;\r
+        props = (typeof props == "object") ? props : {};\r
+        var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";\r
+        var path = props.path ? ";path=" + props.path : "";\r
+        var domain = props.domain ? ";domain=" + props.domain : "";\r
+        var secure = props.secure ? ";secure" : "";\r
+        var serialized = serializeSelection(api.getSelection(win));\r
+        win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;\r
+    }\r
+\r
+    api.serializePosition = serializePosition;\r
+    api.deserializePosition = deserializePosition;\r
+\r
+    api.serializeRange = serializeRange;\r
+    api.deserializeRange = deserializeRange;\r
+    api.canDeserializeRange = canDeserializeRange;\r
+\r
+    api.serializeSelection = serializeSelection;\r
+    api.deserializeSelection = deserializeSelection;\r
+    api.canDeserializeSelection = canDeserializeSelection;\r
+\r
+    api.restoreSelectionFromCookie = restoreSelectionFromCookie;\r
+    api.saveSelectionCookie = saveSelectionCookie;\r
+\r
+    api.getElementChecksum = getElementChecksum;\r
+});\r
diff --git a/Android/webViewMarker/src/main/java/com/blahti/drag/DragController.java b/Android/webViewMarker/src/main/java/com/blahti/drag/DragController.java
new file mode 100755 (executable)
index 0000000..86ab1bc
--- /dev/null
@@ -0,0 +1,366 @@
+/*
+ * This is a modified version of a class from the Android
+ * Open Source Project. The original copyright and license information follows.
+ * 
+ * Copyright (C) 2008 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.blahti.drag;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+
+import java.util.ArrayList;
+
+/**
+ * This class is used to initiate a drag within a view or across multiple views.
+ * When a drag starts it creates a special view (a DragView) that moves around the screen
+ * until the user ends the drag. As feedback to the user, this object causes the device to
+ * vibrate as the drag begins.
+ *
+ */
+
+public class DragController {
+    public enum DragBehavior {
+        MOVE,   // indicates the drag is move
+        COPY    // indicates the drag is copy
+    }
+    public static final String TAG = "DragController";
+
+    private Context mContext;
+    private Rect mRectTemp = new Rect();
+    private final int[] mCoordinatesTemp = new int[2];
+    private boolean mDragging;
+    private float mMotionDownX;
+    private float mMotionDownY;
+    private DisplayMetrics mDisplayMetrics = new DisplayMetrics();
+
+    /** Original view that is being dragged.  */
+    private View mOriginator;
+
+    /** X offset from the upper-left corner of the cell to where we touched.  */
+    private float mTouchOffsetX;
+
+    /** Y offset from the upper-left corner of the cell to where we touched.  */
+    private float mTouchOffsetY;
+
+    /** Where the drag originated */
+    private DragSource mDragSource;
+
+    /** The data associated with the object being dragged */
+    private Object mDragInfo;
+
+    /** The view that moves around while you drag.  */
+    private DragView mDragView;
+
+    /** Who can receive drop events */
+    private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>();
+
+    private DragListener mListener;
+
+    /** The window token used as the parent for the DragView. */
+    private IBinder mWindowToken;
+
+    private View mMoveTarget;
+
+    private DropTarget mLastDropTarget;
+
+    private InputMethodManager mInputMethodManager;
+
+
+    /**
+     * Used to create a new DragLayer from XML.
+     *
+     * @param context The application's context.
+     */
+    public DragController(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Starts a drag. 
+     * It creates a bitmap of the view being dragged. That bitmap is what you see moving.
+     * The actual view can be repositioned if that is what the onDrop handle chooses to do.
+     * 
+     * @param v The view that is being dragged
+     * @param source An object representing where the drag originated
+     * @param dragInfo The data associated with the object that is being dragged
+     * @param dragAction The drag behavior: move or copy
+     */
+    public void startDrag(View v, DragSource source, Object dragInfo, DragBehavior dragBehavior) {
+        if (source.allowDrag()) {
+            mOriginator = v;
+            final Bitmap b = getViewBitmap(v);
+            if (b != null) {
+                final int[] loc = mCoordinatesTemp;
+                v.getLocationOnScreen(loc);
+                final int screenX = loc[0];
+                final int screenY = loc[1];
+                startDrag(b, screenX, screenY, 0, 0, b.getWidth(), b.getHeight(), source, dragInfo, dragBehavior);
+                b.recycle();
+                if (dragBehavior == DragBehavior.MOVE) {
+                    v.setVisibility(View.GONE);
+                }
+            }
+        }
+    }
+
+    /**
+     * Starts a drag.
+     * 
+     * @param b The bitmap to display as the drag image.  It will be re-scaled to the
+     *          enlarged size.
+     * @param screenX The x position on screen of the left-top of the bitmap.
+     * @param screenY The y position on screen of the left-top of the bitmap.
+     * @param textureLeft The left edge of the region inside b to use.
+     * @param textureTop The top edge of the region inside b to use.
+     * @param textureWidth The width of the region inside b to use.
+     * @param textureHeight The height of the region inside b to use.
+     * @param source An object representing where the drag originated
+     * @param dragInfo The data associated with the object that is being dragged
+     * @param dragBehavior The drag action: move or copy
+     */
+    private void startDrag(Bitmap b, int screenX, int screenY, int textureLeft, int textureTop, int textureWidth, int textureHeight, DragSource source, Object dragInfo, DragBehavior dragBehavior) {
+        if (mInputMethodManager == null) {
+            mInputMethodManager = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+        }
+        mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);
+        if (mListener != null) {
+            mListener.onDragStart(source, dragInfo, dragBehavior);
+        }
+        final int registrationX = ((int)mMotionDownX) - screenX;
+        final int registrationY = ((int)mMotionDownY) - screenY;
+        mTouchOffsetX = mMotionDownX - screenX;
+        mTouchOffsetY = mMotionDownY - screenY;
+        mDragging = true;
+        mDragSource = source;
+        mDragInfo = dragInfo;
+        mDragView = new DragView(mContext, b, registrationX, registrationY, textureLeft, textureTop, textureWidth, textureHeight);
+        mDragView.show(mWindowToken, (int)mMotionDownX, (int)mMotionDownY);
+    }
+
+    /**
+     * Draw the view into a bitmap.
+     */
+    private Bitmap getViewBitmap(View v) {
+        v.clearFocus();
+        v.setPressed(false);
+
+        boolean willNotCache = v.willNotCacheDrawing();
+        v.setWillNotCacheDrawing(false);
+
+        // Reset the drawing cache background color to fully transparent
+        // for the duration of this operation
+        int color = v.getDrawingCacheBackgroundColor();
+        v.setDrawingCacheBackgroundColor(0);
+
+        if (color != 0) {
+            v.destroyDrawingCache();
+        }
+        v.buildDrawingCache();
+        Bitmap cacheBitmap = v.getDrawingCache();
+        if (cacheBitmap == null) {
+            Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException());
+            return null;
+        }
+
+        Bitmap bitmap = Bitmap.createBitmap(cacheBitmap);
+
+        // Restore the view
+        v.destroyDrawingCache();
+        v.setWillNotCacheDrawing(willNotCache);
+        v.setDrawingCacheBackgroundColor(color);
+
+        return bitmap;
+    }
+
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mDragging;
+    }
+
+    public void cancelDrag() {
+        endDrag();
+    }
+
+    private void endDrag() {
+        if (mDragging) {
+            mDragging = false;
+            if (mOriginator != null) {
+                mOriginator.setVisibility(View.VISIBLE);
+            }
+            if (mListener != null) {
+                mListener.onDragEnd();
+            }
+            if (mDragView != null) {
+                mDragView.remove();
+                mDragView = null;
+            }
+        }
+    }
+
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        final int action = ev.getAction();
+        if (action == MotionEvent.ACTION_DOWN) {
+            recordScreenSize();
+        }
+        final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels);
+        final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels);
+        switch (action) {
+            case MotionEvent.ACTION_MOVE:
+                break;
+            case MotionEvent.ACTION_DOWN:
+                mMotionDownX = screenX;
+                mMotionDownY = screenY;
+                mLastDropTarget = null;
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                if (mDragging) {
+                    drop(screenX, screenY);
+                }
+                endDrag();
+                break;
+        }
+        return mDragging;
+    }
+
+    /**
+     * Sets the view that should handle move events.
+     */
+    void setMoveTarget(View view) {
+        mMoveTarget = view;
+    }
+
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction);
+    }
+
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!mDragging) {
+            return false;
+        }
+
+        final int action = ev.getAction();
+        final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels);
+        final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels);
+
+        switch (action) {
+        case MotionEvent.ACTION_DOWN:
+            mMotionDownX = screenX;
+            mMotionDownY = screenY;
+            break;
+        case MotionEvent.ACTION_MOVE:
+            mDragView.move((int)ev.getRawX(), (int)ev.getRawY());
+            final int[] coordinates = mCoordinatesTemp;
+            DropTarget dropTarget = findDropTarget(screenX, screenY, coordinates);
+            if (dropTarget != null) {
+                if (mLastDropTarget == dropTarget) {
+                    dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], (int)mTouchOffsetX, (int)mTouchOffsetY, mDragView, mDragInfo);
+                }
+                else {
+                    if (mLastDropTarget != null) {
+                        mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
+                    }
+                    dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], (int)mTouchOffsetX, (int)mTouchOffsetY, mDragView, mDragInfo);
+                }
+            }
+            else {
+                if (mLastDropTarget != null) {
+                    mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int)mTouchOffsetX, (int)mTouchOffsetY, mDragView, mDragInfo);
+                }
+            }
+            mLastDropTarget = dropTarget;
+            break;
+        case MotionEvent.ACTION_UP:
+            if (mDragging) {
+                drop(screenX, screenY);
+            }
+            endDrag();
+            break;
+        case MotionEvent.ACTION_CANCEL:
+            cancelDrag();
+        }
+
+        return true;
+    }
+
+    private boolean drop(float x, float y) {
+        final int[] coordinates = mCoordinatesTemp;
+        final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
+        if (dropTarget != null) {
+            dropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int)mTouchOffsetX, (int)mTouchOffsetY, mDragView, mDragInfo);
+            if (dropTarget.acceptDrop(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo)) {
+                dropTarget.onDrop(mDragSource, coordinates[0], coordinates[1], (int)mTouchOffsetX, (int)mTouchOffsetY, mDragView, mDragInfo);
+                mDragSource.onDropCompleted((View)dropTarget, true);
+            }
+            else {
+                mDragSource.onDropCompleted((View)dropTarget, false);
+            }
+            return true;
+        }
+        return false;
+    }
+    private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
+        final Rect r = mRectTemp;
+        final ArrayList<DropTarget> dropTargets = mDropTargets;
+        final int count = dropTargets.size();
+        for (int i = count - 1; i >= 0; i--) {
+            final DropTarget target = dropTargets.get(i);
+            target.getHitRect(r);
+            target.getLocationOnScreen(dropCoordinates);
+            r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop());
+            if (r.contains(x, y)) {
+                dropCoordinates[0] = x - dropCoordinates[0];
+                dropCoordinates[1] = y - dropCoordinates[1];
+                return target;
+            }
+        }
+        return null;
+    }
+
+    private void recordScreenSize() {
+        ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(mDisplayMetrics);
+    }
+    private static int clamp(int val, int min, int max) {
+        if (val < min) {
+            return min;
+        }
+        else if (val >= max) {
+            return max - 1;
+        }
+        else {
+            return val;
+        }
+    }
+
+    public void setDragListener(DragListener listener) {
+        mListener = listener;
+    }
+    public void addDropTarget(DropTarget target) {
+        mDropTargets.add(target);
+    }
+    public void removeDropTarget(DropTarget target) {
+        mDropTargets.remove(target);
+    }
+}
diff --git a/Android/webViewMarker/src/main/java/com/blahti/drag/DragLayer.java b/Android/webViewMarker/src/main/java/com/blahti/drag/DragLayer.java
new file mode 100755 (executable)
index 0000000..050f657
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * This is a modified version of a class from the Android Open Source Project. 
+ * The original copyright and license information follows.
+ * 
+ * Copyright (C) 2008 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.blahti.drag;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.KeyEvent;
+import android.view.View;
+
+/**
+ * A ViewGroup that coordinates dragging across its descendants.
+ *
+ * <p> This class used DragLayer in the Android Launcher activity as a model.
+ * It is a bit different in several respects:
+ * (1) It extends MyAbsoluteLayout rather than FrameLayout; (2) it implements DragSource and DropTarget methods
+ * that were done in a separate Workspace class in the Launcher.
+ */
+public class DragLayer extends MyAbsoluteLayout implements DragSource, DropTarget {
+    private DragController mDragController;
+
+    public DragLayer (Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
+    }
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return mDragController.onInterceptTouchEvent(ev);
+    }
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        return mDragController.onTouchEvent(ev);
+    }
+    @Override
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        return mDragController.dispatchUnhandledMove(focused, direction);
+    }
+
+    // Interfaces of DragSource
+    @Override
+    public boolean allowDrag() {
+        return true;
+    }
+    @Override
+    public void setDragController(DragController controller) {
+        mDragController = controller;
+    }
+    @Override
+    public void onDropCompleted(View target, boolean success) {
+    }
+
+    // Interfaces of DropTarget
+    @Override
+    public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) {
+        final View v = (View)dragInfo;
+        final int w = v.getWidth();
+        final int h = v.getHeight();
+        final int left = x - xOffset;
+        final int top = y - yOffset;
+        final DragLayer.LayoutParams lp = new DragLayer.LayoutParams (w, h, left, top);
+        updateViewLayout(v, lp);
+    }
+    @Override
+    public void onDragEnter(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) {
+    }
+    @Override
+    public void onDragOver(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) {
+    }
+    @Override
+    public void onDragExit(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) {
+    }
+    @Override
+    public boolean acceptDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) {
+        return true;
+    }
+    @Override
+    public Rect estimateDropLocation(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo, Rect recycle) {
+        return null;
+    }
+}
diff --git a/Android/webViewMarker/src/main/java/com/blahti/drag/DragListener.java b/Android/webViewMarker/src/main/java/com/blahti/drag/DragListener.java
new file mode 100755 (executable)
index 0000000..fe100d8
--- /dev/null
@@ -0,0 +1,9 @@
+package com.blahti.drag;
+
+/**
+ * Interface to receive notifications when a drag starts or stops
+ */
+public interface DragListener {
+    void onDragStart(DragSource source, Object info, DragController.DragBehavior dragBehavior);
+    void onDragEnd();
+}
diff --git a/Android/webViewMarker/src/main/java/com/blahti/drag/DragSource.java b/Android/webViewMarker/src/main/java/com/blahti/drag/DragSource.java
new file mode 100755 (executable)
index 0000000..d0d8a35
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * This is a modified version of a class from the Android Open Source Project. 
+ * The original copyright and license information follows.
+ * 
+ * Copyright (C) 2008 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.blahti.drag;
+
+import android.view.View;
+
+/**
+ * Interface defining an object where drag operations originate.
+ *
+ */
+public interface DragSource {
+
+    /**
+     * This method is called to determine if the DragSource has something to drag.
+     * 
+     * @return True if there is something to drag
+     */
+
+    boolean allowDrag ();
+
+    /**
+     * This method is used to tell the DragSource which drag controller it is working with.
+     * 
+     * @param dragger DragController
+     */
+
+    void setDragController(DragController dragger);
+
+    /**
+     * This method is called on the completion of the drag operation so the DragSource knows 
+     * whether it succeeded or failed.
+     * 
+     * @param target View - the view that accepted the dragged object
+     * @param success boolean - true means that the object was dropped successfully
+     */
+
+    void onDropCompleted (View target, boolean success);
+}
diff --git a/Android/webViewMarker/src/main/java/com/blahti/drag/DragView.java b/Android/webViewMarker/src/main/java/com/blahti/drag/DragView.java
new file mode 100755 (executable)
index 0000000..ad1de4c
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * This is a modified version of a class from the Android Open Source Project. 
+ * The original copyright and license information follows.
+ * 
+ * Copyright (C) 2008 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.blahti.drag;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+/**
+ * A DragView is a special view used by a DragController. During a drag operation, what is actually moving
+ * on the screen is a DragView. A DragView is constructed using a bitmap of the view the user really
+ * wants to move.
+ *
+ */
+
+public class DragView extends View {
+    private static final boolean DEBUG = false;
+    private static final int PADDING_TO_SCALE = 0;
+    private final int mRegistrationX;
+    private final int mRegistrationY;
+    private Bitmap mBitmap;
+    private Paint mDebugPaint = new Paint();
+    private WindowManager.LayoutParams mLayoutParams;
+    private WindowManager mWindowManager;
+
+    public DragView(Context context) throws Exception {
+        super(context);
+        mRegistrationX = 0;
+        mRegistrationY = 0;
+        throw new Exception("DragView constructor permits only programatical calling");
+    }
+
+    /**
+     * Construct the drag view.
+     * <p>
+     * The registration point is the point inside our view that the touch events should
+     * be centered upon.
+     *
+     * @param context A context
+     * @param bitmap The view that we're dragging around.  We scale it up when we draw it.
+     * @param registrationX The x coordinate of the registration point.
+     * @param registrationY The y coordinate of the registration point.
+     */
+    public DragView(Context context, Bitmap bitmap, int registrationX, int registrationY, int left, int top, int width, int height) {
+        super(context);
+        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);        
+        mRegistrationX = registrationX + (PADDING_TO_SCALE / 2);
+        mRegistrationY = registrationY + (PADDING_TO_SCALE / 2);
+        final float scaleFactor = ((float)width + PADDING_TO_SCALE) / (float)width;
+        final Matrix scale = new Matrix();
+        scale.setScale(scaleFactor, scaleFactor);
+        mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height, scale, true);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight());
+    }
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (DEBUG) {
+            mDebugPaint.setStyle(Paint.Style.FILL);
+            mDebugPaint.setColor(0x88dd0011);
+            canvas.drawRect(0, 0, getWidth(), getHeight(), mDebugPaint);
+        }
+        canvas.drawBitmap(mBitmap, 0.0f, 0.0f, null);
+    }
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mBitmap.recycle();
+    }
+
+    void show(IBinder windowToken, int touchX, int touchY) {
+        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+            ViewGroup.LayoutParams.WRAP_CONTENT,
+            ViewGroup.LayoutParams.WRAP_CONTENT,
+            touchX - mRegistrationX, touchY - mRegistrationY,
+            WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL,
+            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+            PixelFormat.TRANSLUCENT
+        );
+        lp.gravity = Gravity.LEFT | Gravity.TOP;
+        lp.token = windowToken;
+        lp.setTitle("DragView");
+        mLayoutParams = lp;
+        mWindowManager.addView(this, lp);
+    }
+    void move(int touchX, int touchY) {
+        WindowManager.LayoutParams lp = mLayoutParams;
+        lp.x = touchX - mRegistrationX;
+        lp.y = touchY - mRegistrationY;
+        mWindowManager.updateViewLayout(this, lp);
+    }
+    void remove() {
+        mWindowManager.removeView(this);
+    }
+}
diff --git a/Android/webViewMarker/src/main/java/com/blahti/drag/DropTarget.java b/Android/webViewMarker/src/main/java/com/blahti/drag/DropTarget.java
new file mode 100755 (executable)
index 0000000..9f5b394
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * This is a modified version of a class from the Android Open Source Project. 
+ * The original copyright and license information follows.
+ * 
+ * Copyright (C) 2008 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.blahti.drag;
+
+import android.graphics.Rect;
+
+/**
+ * Interface defining an object that can receive a view at the end of a drag operation.
+ *
+ */
+public interface DropTarget {
+
+    /**
+     * Handle an object being dropped on the DropTarget
+     * 
+     * @param source DragSource where the drag started
+     * @param x X coordinate of the drop location
+     * @param y Y coordinate of the drop location
+     * @param xOffset Horizontal offset with the object being dragged where the original
+     *          touch happened
+     * @param yOffset Vertical offset with the object being dragged where the original
+     *          touch happened
+     * @param dragView The DragView that's being dragged around on screen.
+     * @param dragInfo Data associated with the object being dragged
+     * 
+     */
+    void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo);
+
+    void onDragEnter(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo);
+
+    void onDragOver(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo);
+
+    void onDragExit(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo);
+
+    /**
+     * Check if a drop action can occur at, or near, the requested location.
+     * This may be called repeatedly during a drag, so any calls should return
+     * quickly.
+     * 
+     * @param source DragSource where the drag started
+     * @param x X coordinate of the drop location
+     * @param y Y coordinate of the drop location
+     * @param xOffset Horizontal offset with the object being dragged where the
+     *            original touch happened
+     * @param yOffset Vertical offset with the object being dragged where the
+     *            original touch happened
+     * @param dragView The DragView that's being dragged around on screen.
+     * @param dragInfo Data associated with the object being dragged
+     * @return True if the drop will be accepted, false otherwise.
+     */
+    boolean acceptDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo);
+
+    /**
+     * Estimate the surface area where this object would land if dropped at the
+     * given location.
+     * 
+     * @param source DragSource where the drag started
+     * @param x X coordinate of the drop location
+     * @param y Y coordinate of the drop location
+     * @param xOffset Horizontal offset with the object being dragged where the
+     *            original touch happened
+     * @param yOffset Vertical offset with the object being dragged where the
+     *            original touch happened
+     * @param dragView The DragView that's being dragged around on screen.
+     * @param dragInfo Data associated with the object being dragged
+     * @param recycle {@link Rect} object to be possibly recycled.
+     * @return Estimated area that would be occupied if object was dropped at
+     *         the given location. Should return null if no estimate is found,
+     *         or if this target doesn't provide estimations.
+     */
+    Rect estimateDropLocation(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo, Rect recycle);
+
+    // These methods are implemented in Views
+    void getHitRect(Rect outRect);
+    void getLocationOnScreen(int[] loc);
+    int getLeft();
+    int getTop();
+}
diff --git a/Android/webViewMarker/src/main/java/com/blahti/drag/MyAbsoluteLayout.java b/Android/webViewMarker/src/main/java/com/blahti/drag/MyAbsoluteLayout.java
new file mode 100755 (executable)
index 0000000..ae709ec
--- /dev/null
@@ -0,0 +1,250 @@
+/*
+ * This is a modified version of a class from the Android Open Source Project. 
+ * The original copyright and license information follows.
+ * 
+ * Copyright (C) 2006 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.blahti.drag;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * A layout that lets you specify exact locations (x/y coordinates) of its
+ * children. Absolute layouts are less flexible and harder to maintain than
+ * other types of layouts without absolute positioning.
+ *
+ * <p><strong>XML attributes</strong></p> <p> See {@link
+ * android.R.styleable#ViewGroup ViewGroup Attributes}, {@link
+ * android.R.styleable#View View Attributes}</p>
+ * 
+ * <p>Note: This class is a clone of AbsoluteLayout, which is now deprecated.
+ */
+
+@RemoteView
+public class MyAbsoluteLayout extends ViewGroup {
+    public MyAbsoluteLayout(Context context) {
+        super(context);
+    }
+
+    public MyAbsoluteLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MyAbsoluteLayout(Context context, AttributeSet attrs,
+            int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int count = getChildCount();
+
+        int maxHeight = 0;
+        int maxWidth = 0;
+
+        // Find out how big everyone wants to be
+        measureChildren(widthMeasureSpec, heightMeasureSpec);
+
+        // Find rightmost and bottom-most child
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                int childRight;
+                int childBottom;
+
+                MyAbsoluteLayout.LayoutParams lp
+                        = (MyAbsoluteLayout.LayoutParams) child.getLayoutParams();
+
+                childRight = lp.x + child.getMeasuredWidth();
+                childBottom = lp.y + child.getMeasuredHeight();
+
+                maxWidth = Math.max(maxWidth, childRight);
+                maxHeight = Math.max(maxHeight, childBottom);
+            }
+        }
+
+        // Account for padding too
+        maxWidth += getPaddingLeft () + getPaddingRight ();
+        maxHeight += getPaddingTop () + getPaddingBottom ();
+        /* original
+        maxWidth += mPaddingLeft + mPaddingRight;
+        maxHeight += mPaddingTop + mPaddingBottom;
+        */
+
+        // Check against minimum height and width
+        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+        setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
+                resolveSize(maxHeight, heightMeasureSpec));
+    }
+
+    /**
+     * Returns a set of layout parameters with a width of
+     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT},
+     * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+     * and with the coordinates (0, 0).
+     */
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t,
+            int r, int b) {
+        int count = getChildCount();
+
+        int paddingL = getPaddingLeft ();
+        int paddingT = getPaddingTop ();
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+
+                MyAbsoluteLayout.LayoutParams lp =
+                        (MyAbsoluteLayout.LayoutParams) child.getLayoutParams();
+
+                int childLeft = paddingL + lp.x;
+                int childTop = paddingT + lp.y;
+                /*
+                int childLeft = mPaddingLeft + lp.x;
+                int childTop = mPaddingTop + lp.y;
+                */
+                child.layout(childLeft, childTop,
+                        childLeft + child.getMeasuredWidth(),
+                        childTop + child.getMeasuredHeight());
+
+            }
+        }
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new MyAbsoluteLayout.LayoutParams(getContext(), attrs);
+    }
+
+    // Override to allow type-checking of LayoutParams. 
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof MyAbsoluteLayout.LayoutParams;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        return new LayoutParams(p);
+    }
+
+    /**
+     * Per-child layout information associated with MyAbsoluteLayout.
+     * See
+     * {@link android.R.styleable#MyAbsoluteLayout_Layout Absolute Layout Attributes}
+     * for a list of all child view attributes that this class supports.
+     */
+    public static class LayoutParams extends ViewGroup.LayoutParams {
+        /**
+         * The horizontal, or X, location of the child within the view group.
+         */
+        public int x;
+        /**
+         * The vertical, or Y, location of the child within the view group.
+         */
+        public int y;
+
+        /**
+         * Creates a new set of layout parameters with the specified width,
+         * height and location.
+         *
+         * @param width the width, either {@link #MATCH_PARENT},
+                  {@link #WRAP_CONTENT} or a fixed size in pixels
+         * @param height the height, either {@link #MATCH_PARENT},
+                  {@link #WRAP_CONTENT} or a fixed size in pixels
+         * @param x the X location of the child
+         * @param y the Y location of the child
+         */
+        public LayoutParams(int width, int height, int x, int y) {
+            super(width, height);
+            this.x = x;
+            this.y = y;
+        }
+
+        /**
+         * Creates a new set of layout parameters. The values are extracted from
+         * the supplied attributes set and context. The XML attributes mapped
+         * to this set of layout parameters are:
+         *
+         * <ul>
+         *   <li><code>layout_x</code>: the X location of the child</li>
+         *   <li><code>layout_y</code>: the Y location of the child</li>
+         *   <li>All the XML attributes from
+         *   {@link android.view.ViewGroup.LayoutParams}</li>
+         * </ul>
+         *
+         * @param c the application environment
+         * @param attrs the set of attributes from which to extract the layout
+         *              parameters values
+         */
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+            /* FIX THIS eventually. Without this, I don't think you can put x and y in layout xml files.
+            TypedArray a = c.obtainStyledAttributes(attrs,
+                    com.android.internal.R.styleable.AbsoluteLayout_Layout);
+            x = a.getDimensionPixelOffset(
+                    com.android.internal.R.styleable.AbsoluteLayout_Layout_layout_x, 0);
+            y = a.getDimensionPixelOffset(
+                    com.android.internal.R.styleable.AbsoluteLayout_Layout_layout_y, 0);
+            a.recycle();
+            */
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+
+        public String debug(String output) {
+            return output + "Absolute.LayoutParams={width="
+                    + sizeToString(width) + ", height=" + sizeToString(height)
+                    + " x=" + x + " y=" + y + "}";
+        }
+
+      /**
+         * Converts the specified size to a readable String.
+         *
+         * @param size the size to convert
+         * @return a String instance representing the supplied size
+         *
+         * @hide
+         */
+        protected static String sizeToString(int size) {
+            if (size == WRAP_CONTENT) {
+                return "wrap-content";
+            }
+            if (size == MATCH_PARENT) {
+                return "match-parent";
+            }
+            return String.valueOf(size);
+        }
+    } // end class
+
+} // end class
+
+
diff --git a/Android/webViewMarker/src/main/java/com/bossturban/webviewmarker/TextSelectionControlListener.java b/Android/webViewMarker/src/main/java/com/bossturban/webviewmarker/TextSelectionControlListener.java
new file mode 100755 (executable)
index 0000000..4fc4494
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2012 - 2014 Brandon Tate, bossturbo
+ *
+ * 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.bossturban.webviewmarker;
+
+public interface TextSelectionControlListener {
+    void jsError(String error);
+    void jsLog(String message);
+    void startSelectionMode();
+    void endSelectionMode();
+
+    /**
+     * Tells the listener to show the context menu for the given range and selected text.
+     * The bounds parameter contains a json string representing the selection bounds in the form 
+     * { 'left': leftPoint, 'top': topPoint, 'right': rightPoint, 'bottom': bottomPoint }
+     * @param range
+     * @param text
+     * @param handleBounds
+     * @param isReallyChanged
+     */
+    void selectionChanged(String range, String text, String handleBounds, boolean isReallyChanged);
+
+    void setContentWidth(float contentWidth);
+}
\ No newline at end of file
diff --git a/Android/webViewMarker/src/main/java/com/bossturban/webviewmarker/TextSelectionController.java b/Android/webViewMarker/src/main/java/com/bossturban/webviewmarker/TextSelectionController.java
new file mode 100755 (executable)
index 0000000..fc313c9
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 - 2014 Brandon Tate, bossturbo
+ *
+ * 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.bossturban.webviewmarker;
+
+import android.webkit.JavascriptInterface;
+
+public class TextSelectionController {
+    public static final String TAG = "TextSelectionController";
+    public static final String INTERFACE_NAME = "TextSelection";
+
+    private TextSelectionControlListener mListener;
+
+    public TextSelectionController(TextSelectionControlListener listener) {
+        mListener = listener;
+    }
+
+    @JavascriptInterface
+    public void jsError(String error) {
+        if (mListener != null) {
+            mListener.jsError(error);
+        }
+    }
+    @JavascriptInterface
+    public void jsLog(String message) {
+        if (mListener != null) {
+            mListener.jsLog(message);
+        }
+    }
+    @JavascriptInterface
+    public void startSelectionMode() {
+        if (mListener != null) {
+            mListener.startSelectionMode();
+        }
+    }
+    @JavascriptInterface
+    public void endSelectionMode() {
+        if (mListener != null) {
+            mListener.endSelectionMode();
+        }
+    }
+    @JavascriptInterface
+    public void selectionChanged(String range, String text, String handleBounds, boolean isReallyChanged) {
+        if (mListener != null) {
+            mListener.selectionChanged(range, text, handleBounds, isReallyChanged);
+        }
+    }
+    @JavascriptInterface
+    public void setContentWidth(float contentWidth) {
+        if (mListener != null) {
+            mListener.setContentWidth(contentWidth);
+        }
+    }
+}
diff --git a/Android/webViewMarker/src/main/java/com/bossturban/webviewmarker/TextSelectionSupport.java b/Android/webViewMarker/src/main/java/com/bossturban/webviewmarker/TextSelectionSupport.java
new file mode 100755 (executable)
index 0000000..456f7ef
--- /dev/null
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2012 - 2014 Brandon Tate, bossturbo
+ *
+ * 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.bossturban.webviewmarker;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.blahti.drag.DragController;
+import com.blahti.drag.DragController.DragBehavior;
+import com.blahti.drag.DragLayer;
+import com.blahti.drag.DragListener;
+import com.blahti.drag.DragSource;
+import com.blahti.drag.MyAbsoluteLayout;
+import com.bossturban.webviewmarker.R;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.widget.ImageView;
+
+import java.util.Locale;
+
+@SuppressLint("DefaultLocale")
+public class TextSelectionSupport implements TextSelectionControlListener, OnTouchListener, OnLongClickListener, DragListener {
+    public interface SelectionListener {
+        void startSelection();
+        void selectionChanged(String text);
+        void endSelection();
+    }
+
+    private enum HandleType {
+        START,
+        END,
+        UNKNOWN
+    }
+    private static final String TAG = "SelectionSupport";
+    private static final float CENTERING_SHORTER_MARGIN_RATIO = 12.0f / 48.0f;
+    private static final int JACK_UP_PADDING = 2;
+    private static final int SCROLLING_THRESHOLD = 10;
+
+    private Activity mActivity;
+    private WebView mWebView;
+    private SelectionListener mSelectionListener;
+    private DragLayer mSelectionDragLayer;
+    private DragController mDragController;
+    private ImageView mStartSelectionHandle;
+    private ImageView mEndSelectionHandle;
+    private Rect mSelectionBounds = null;
+    private final Rect mSelectionBoundsTemp = new Rect();
+    private TextSelectionController mSelectionController = null;
+    private int mContentWidth = 0;
+    private HandleType mLastTouchedSelectionHandle = HandleType.UNKNOWN;
+    private boolean mScrolling = false;
+    private float mScrollDiffY = 0;
+    private float mLastTouchY = 0;
+    private float mScrollDiffX = 0;
+    private float mLastTouchX = 0;
+    private float mScale = 1.0f;
+
+    private Runnable mStartSelectionModeHandler = new Runnable() {
+        public void run() {
+            if (mSelectionBounds != null) {
+                mWebView.addView(mSelectionDragLayer);
+                drawSelectionHandles();
+                final int contentHeight = (int)Math.ceil(getDensityDependentValue(mWebView.getContentHeight(), mActivity));
+                final int contentWidth = mWebView.getWidth();
+                ViewGroup.LayoutParams layerParams = mSelectionDragLayer.getLayoutParams();
+                layerParams.height = contentHeight;
+                layerParams.width = Math.max(contentWidth, mContentWidth);
+                mSelectionDragLayer.setLayoutParams(layerParams);
+                if (mSelectionListener != null) {
+                    mSelectionListener.startSelection();
+                }
+            }
+        }
+    };
+    private Runnable endSelectionModeHandler = new Runnable(){
+        public void run() {
+            mWebView.removeView(mSelectionDragLayer);
+            mSelectionBounds = null;
+            mLastTouchedSelectionHandle = HandleType.UNKNOWN;
+            mWebView.loadUrl("javascript: android.selection.clearSelection();");
+            if (mSelectionListener != null) {
+                mSelectionListener.endSelection();
+            }
+        }
+    };
+
+    private TextSelectionSupport(Activity activity, WebView webview) {
+        mActivity = activity;
+        mWebView = webview;
+    }
+    public static TextSelectionSupport support(Activity activity, WebView webview) {
+        final TextSelectionSupport selectionSupport = new TextSelectionSupport(activity, webview);
+        selectionSupport.setup();
+        return selectionSupport;
+    }
+
+    public void onScaleChanged(float oldScale, float newScale) {
+        mScale = newScale;
+    }
+    public void setSelectionListener(SelectionListener listener) {
+        mSelectionListener = listener;
+    }
+
+    //
+    // Interfaces of TextSelectionControlListener
+    //
+    @Override
+    public void jsError(String error) {
+        Log.e(TAG, "JSError: " + error);
+    }
+    @Override
+    public void jsLog(String message) {
+        Log.d(TAG, "JSLog: " + message);
+    }
+    @Override
+    public void startSelectionMode() {
+        mActivity.runOnUiThread(mStartSelectionModeHandler);
+    }
+    @Override
+    public void endSelectionMode() {
+        mActivity.runOnUiThread(endSelectionModeHandler);
+    }
+    @Override
+    public void setContentWidth(float contentWidth){
+        mContentWidth = (int)getDensityDependentValue(contentWidth, mActivity);
+    }
+    @Override
+    public void selectionChanged(String range, String text, String handleBounds, boolean isReallyChanged){
+        final Context ctx = mActivity;
+        try {
+            final JSONObject selectionBoundsObject = new JSONObject(handleBounds);
+            final float scale = getDensityIndependentValue(mScale, ctx);
+            Rect rect = mSelectionBoundsTemp;
+            rect.left = (int)(getDensityDependentValue(selectionBoundsObject.getInt("left"), ctx) * scale);
+            rect.top = (int)(getDensityDependentValue(selectionBoundsObject.getInt("top"), ctx) * scale);
+            rect.right = (int)(getDensityDependentValue(selectionBoundsObject.getInt("right"), ctx) * scale);
+            rect.bottom = (int)(getDensityDependentValue(selectionBoundsObject.getInt("bottom"), ctx) * scale);
+            mSelectionBounds = rect;
+            if (!isInSelectionMode()){
+                startSelectionMode();
+            }
+            drawSelectionHandles();
+            if (mSelectionListener != null && isReallyChanged) {
+                mSelectionListener.selectionChanged(text);
+            }
+        }
+        catch (JSONException e) {
+            e.printStackTrace();
+        }
+    }
+
+    //
+    // Interface of OnTouchListener
+    //
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        final Context ctx = mActivity;
+        float xPoint = getDensityIndependentValue(event.getX(), ctx) / getDensityIndependentValue(mScale, ctx);
+        float yPoint = getDensityIndependentValue(event.getY(), ctx) / getDensityIndependentValue(mScale, ctx);
+
+        switch (event.getAction()) {
+        case MotionEvent.ACTION_DOWN:
+            // Essential to add Locale.US parameter to String.format, else does not work on systems
+            // with default locale different, with other floating point notations, e.g. comma instead
+            // of decimal point.
+            final String startTouchUrl = String.format(Locale.US, "javascript:android.selection.startTouch(%f, %f);", xPoint, yPoint);
+            mLastTouchX = xPoint;
+            mLastTouchY = yPoint;
+            mWebView.loadUrl(startTouchUrl);
+            break;
+        case MotionEvent.ACTION_UP:
+            if (!mScrolling) {
+                endSelectionMode();
+                //
+                // Fixes 4.4 double selection
+                // See: http://stackoverflow.com/questions/20391783/how-to-avoid-default-selection-on-long-press-in-android-kitkat-4-4
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                    return false;
+                }
+            }
+            mScrollDiffX = 0;
+            mScrollDiffY = 0;
+            mScrolling = false;
+            //
+            // Fixes 4.4 double selection
+            // See: http://stackoverflow.com/questions/20391783/how-to-avoid-default-selection-on-long-press-in-android-kitkat-4-4
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && isInSelectionMode()) {
+               return true;
+            }
+            break;
+        case MotionEvent.ACTION_MOVE:
+            mScrollDiffX += (xPoint - mLastTouchX);
+            mScrollDiffY += (yPoint - mLastTouchY);
+            mLastTouchX = xPoint;
+            mLastTouchY = yPoint;
+            if (Math.abs(mScrollDiffX) > SCROLLING_THRESHOLD || Math.abs(mScrollDiffY) > SCROLLING_THRESHOLD) {
+                mScrolling = true;
+            }
+            break;
+        }
+        return false;
+    }
+
+    //
+    // Interface of OnLongClickListener
+    //
+    @Override 
+    public boolean onLongClick(View v){
+        if (!isInSelectionMode()) {
+            mWebView.loadUrl("javascript:android.selection.longTouch();");
+            mScrolling = true;
+        }
+        return true;
+    }
+
+    //
+    // Interface of DragListener
+    //
+    @Override
+    public void onDragStart(DragSource source, Object info, DragBehavior dragBehavior) {
+    }
+    @Override
+    public void onDragEnd() {
+        mActivity.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                MyAbsoluteLayout.LayoutParams startHandleParams = (MyAbsoluteLayout.LayoutParams)mStartSelectionHandle.getLayoutParams();
+                MyAbsoluteLayout.LayoutParams endHandleParams = (MyAbsoluteLayout.LayoutParams)mEndSelectionHandle.getLayoutParams();
+                final Context ctx = mActivity;
+                final float scale = getDensityIndependentValue(mScale, ctx);
+                float startX = startHandleParams.x - mWebView.getScrollX() + mStartSelectionHandle.getWidth() * (1 - CENTERING_SHORTER_MARGIN_RATIO);
+                float startY = startHandleParams.y - mWebView.getScrollY() - JACK_UP_PADDING;
+                float endX = endHandleParams.x - mWebView.getScrollX() + mEndSelectionHandle.getWidth() * CENTERING_SHORTER_MARGIN_RATIO;
+                float endY = endHandleParams.y - mWebView.getScrollY() - JACK_UP_PADDING;
+                startX = getDensityIndependentValue(startX, ctx) / scale;
+                startY = getDensityIndependentValue(startY, ctx) / scale;
+                endX = getDensityIndependentValue(endX, ctx) / scale;
+                endY = getDensityIndependentValue(endY, ctx) / scale;
+                if (mLastTouchedSelectionHandle == HandleType.START && startX > 0 && startY > 0){
+                    String saveStartString = String.format(Locale.US, "javascript: android.selection.setStartPos(%f, %f);", startX, startY);
+                    mWebView.loadUrl(saveStartString);
+                }
+                else if (mLastTouchedSelectionHandle == HandleType.END && endX > 0 && endY > 0){
+                    String saveEndString = String.format(Locale.US, "javascript: android.selection.setEndPos(%f, %f);", endX, endY);
+                    mWebView.loadUrl(saveEndString);
+                }
+                else {
+                    mWebView.loadUrl("javascript: android.selection.restoreStartEndPos();");
+                }
+            }
+        });
+    }
+
+    @SuppressLint("SetJavaScriptEnabled")
+    private void setup(){
+        mScale = mActivity.getResources().getDisplayMetrics().density;
+        mWebView.setOnLongClickListener(this);
+        mWebView.setOnTouchListener(this);
+        final WebSettings settings = mWebView.getSettings();
+        settings.setJavaScriptEnabled(true);
+        settings.setJavaScriptCanOpenWindowsAutomatically(true);
+        mSelectionController = new TextSelectionController(this);
+        mWebView.addJavascriptInterface(mSelectionController, TextSelectionController.INTERFACE_NAME);
+        createSelectionLayer(mActivity);
+    }
+    private void createSelectionLayer(Context context){
+        final LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mSelectionDragLayer = (DragLayer)inflater.inflate(R.layout.selection_drag_layer, null);
+        mDragController = new DragController(context);
+        mDragController.setDragListener(this);
+        mDragController.addDropTarget(mSelectionDragLayer);
+        mSelectionDragLayer.setDragController(mDragController);
+        mStartSelectionHandle = (ImageView)mSelectionDragLayer.findViewById(R.id.startHandle);
+        mStartSelectionHandle.setTag(HandleType.START);
+        mEndSelectionHandle = (ImageView)mSelectionDragLayer.findViewById(R.id.endHandle);
+        mEndSelectionHandle.setTag(HandleType.END);
+        final OnTouchListener handleTouchListener = new OnTouchListener(){
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                boolean handledHere = false;
+                if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                    handledHere = startDrag(v);
+                    mLastTouchedSelectionHandle = (HandleType)v.getTag();
+                }
+                return handledHere;
+            }
+        };
+        mStartSelectionHandle.setOnTouchListener(handleTouchListener);
+        mEndSelectionHandle.setOnTouchListener(handleTouchListener);
+    }
+    private void drawSelectionHandles(){
+        mActivity.runOnUiThread(drawSelectionHandlesHandler);
+    }
+    private Runnable drawSelectionHandlesHandler = new Runnable(){
+        public void run() {
+            MyAbsoluteLayout.LayoutParams startParams = (com.blahti.drag.MyAbsoluteLayout.LayoutParams)mStartSelectionHandle.getLayoutParams();
+            final int startWidth = mStartSelectionHandle.getDrawable().getIntrinsicWidth();
+            startParams.x = (int)(mSelectionBounds.left - startWidth * (1.0f - CENTERING_SHORTER_MARGIN_RATIO));
+            startParams.y = (int)(mSelectionBounds.top);
+            final int startMinLeft = -(int)(startWidth * (1 - CENTERING_SHORTER_MARGIN_RATIO));
+            startParams.x = (startParams.x < startMinLeft) ? startMinLeft : startParams.x;
+            startParams.y = (startParams.y < 0) ? 0 : startParams.y;
+            mStartSelectionHandle.setLayoutParams(startParams);
+
+            MyAbsoluteLayout.LayoutParams endParams = (com.blahti.drag.MyAbsoluteLayout.LayoutParams)mEndSelectionHandle.getLayoutParams();
+            final int endWidth = mEndSelectionHandle.getDrawable().getIntrinsicWidth();
+            endParams.x = (int) (mSelectionBounds.right - endWidth * CENTERING_SHORTER_MARGIN_RATIO);
+            endParams.y = (int) (mSelectionBounds.bottom);
+            final int endMinLeft = -(int)(endWidth * (1- CENTERING_SHORTER_MARGIN_RATIO));
+            endParams.x = (endParams.x < endMinLeft) ? endMinLeft : endParams.x;
+            endParams.y = (endParams.y < 0) ? 0 : endParams.y;
+            mEndSelectionHandle.setLayoutParams(endParams);
+        }
+    };
+
+    private boolean isInSelectionMode(){
+        return this.mSelectionDragLayer.getParent() != null;
+    }
+    private boolean startDrag(View v) {
+        Object dragInfo = v;
+        mDragController.startDrag(v, mSelectionDragLayer, dragInfo, DragBehavior.MOVE);
+        return true;
+    }
+
+    private float getDensityDependentValue(float val, Context ctx){
+        Display display = ((WindowManager)ctx.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+        DisplayMetrics metrics = new DisplayMetrics();
+        display.getMetrics(metrics);
+        return val * (metrics.densityDpi / 160f);
+    }
+    private float getDensityIndependentValue(float val, Context ctx){
+        Display display = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+        DisplayMetrics metrics = new DisplayMetrics();
+        display.getMetrics(metrics);
+        return val / (metrics.densityDpi / 160f);
+    }
+}
\ No newline at end of file
diff --git a/Android/webViewMarker/src/main/res/drawable-hdpi/text_select_handle_left.png b/Android/webViewMarker/src/main/res/drawable-hdpi/text_select_handle_left.png
new file mode 100755 (executable)
index 0000000..d2ed06d
Binary files /dev/null and b/Android/webViewMarker/src/main/res/drawable-hdpi/text_select_handle_left.png differ
diff --git a/Android/webViewMarker/src/main/res/drawable-hdpi/text_select_handle_right.png b/Android/webViewMarker/src/main/res/drawable-hdpi/text_select_handle_right.png
new file mode 100755 (executable)
index 0000000..e419249
Binary files /dev/null and b/Android/webViewMarker/src/main/res/drawable-hdpi/text_select_handle_right.png differ
diff --git a/Android/webViewMarker/src/main/res/drawable-mdpi/text_select_handle_left.png b/Android/webViewMarker/src/main/res/drawable-mdpi/text_select_handle_left.png
new file mode 100755 (executable)
index 0000000..750cdea
Binary files /dev/null and b/Android/webViewMarker/src/main/res/drawable-mdpi/text_select_handle_left.png differ
diff --git a/Android/webViewMarker/src/main/res/drawable-mdpi/text_select_handle_right.png b/Android/webViewMarker/src/main/res/drawable-mdpi/text_select_handle_right.png
new file mode 100755 (executable)
index 0000000..fc3d144
Binary files /dev/null and b/Android/webViewMarker/src/main/res/drawable-mdpi/text_select_handle_right.png differ
diff --git a/Android/webViewMarker/src/main/res/drawable-xhdpi/text_select_handle_left.png b/Android/webViewMarker/src/main/res/drawable-xhdpi/text_select_handle_left.png
new file mode 100755 (executable)
index 0000000..98d10c9
Binary files /dev/null and b/Android/webViewMarker/src/main/res/drawable-xhdpi/text_select_handle_left.png differ
diff --git a/Android/webViewMarker/src/main/res/drawable-xhdpi/text_select_handle_right.png b/Android/webViewMarker/src/main/res/drawable-xhdpi/text_select_handle_right.png
new file mode 100755 (executable)
index 0000000..b3a0c9f
Binary files /dev/null and b/Android/webViewMarker/src/main/res/drawable-xhdpi/text_select_handle_right.png differ
diff --git a/Android/webViewMarker/src/main/res/layout/selection_drag_layer.xml b/Android/webViewMarker/src/main/res/layout/selection_drag_layer.xml
new file mode 100755 (executable)
index 0000000..a09dff5
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.blahti.drag.DragLayer 
+       xmlns:android="http://schemas.android.com/apk/res/android"
+           android:id="@+id/dragLayer"
+           android:layout_width="match_parent"
+           android:layout_height="match_parent"
+           android:background="@android:color/transparent" >
+       
+           <ImageView
+                   android:id="@+id/startHandle"
+                   android:src="@drawable/text_select_handle_left"
+                   android:layout_width="wrap_content"
+                   android:layout_height="wrap_content"
+                   android:scaleType="center" />
+               <ImageView
+                   android:id="@+id/endHandle"
+                   android:src="@drawable/text_select_handle_right"
+                   android:layout_width="wrap_content"
+                   android:layout_height="wrap_content"
+                   android:scaleType="center" />
+       
+       </com.blahti.drag.DragLayer>
diff --git a/Android/webViewMarker/src/main/res/values/strings.xml b/Android/webViewMarker/src/main/res/values/strings.xml
new file mode 100755 (executable)
index 0000000..55344e5
--- /dev/null
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
\ No newline at end of file
diff --git a/Android/webViewMarker/src/main/res/values/styles.xml b/Android/webViewMarker/src/main/res/values/styles.xml
new file mode 100755 (executable)
index 0000000..55344e5
--- /dev/null
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
\ No newline at end of file