Repository: 47deg/nine-cards-v2 Branch: master Commit: 1cbab6cd94a3 Files: 950 Total size: 4.0 MB Directory structure: gitextract_ztr7dq7x/ ├── .gitignore ├── .scalafmt.conf ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── README_old.md ├── codecov.yml ├── modules/ │ ├── api/ │ │ ├── build.sbt │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── res/ │ │ │ │ └── values/ │ │ │ │ └── strings.xml │ │ │ └── scala/ │ │ │ └── cards/ │ │ │ └── nine/ │ │ │ └── api/ │ │ │ ├── rest/ │ │ │ │ └── client/ │ │ │ │ ├── Exceptions.scala │ │ │ │ ├── ServiceClient.scala │ │ │ │ ├── http/ │ │ │ │ │ ├── Exceptions.scala │ │ │ │ │ ├── HttpClient.scala │ │ │ │ │ ├── HttpClientResponse.scala │ │ │ │ │ └── OkHttpClient.scala │ │ │ │ └── messages/ │ │ │ │ └── Messages.scala │ │ │ ├── version1/ │ │ │ │ ├── ApiService.scala │ │ │ │ ├── JsonImplicits.scala │ │ │ │ └── Model.scala │ │ │ └── version2/ │ │ │ ├── ApiService.scala │ │ │ ├── JsonImplicits.scala │ │ │ └── Model.scala │ │ └── test/ │ │ └── scala/ │ │ ├── cards/ │ │ │ └── nine/ │ │ │ └── api/ │ │ │ ├── rest/ │ │ │ │ └── client/ │ │ │ │ ├── Messages.scala │ │ │ │ ├── ServiceClientData.scala │ │ │ │ ├── ServiceClientSpec.scala │ │ │ │ └── http/ │ │ │ │ └── OkHttpClientSpec.scala │ │ │ ├── version1/ │ │ │ │ ├── ApiServiceData.scala │ │ │ │ └── ApiServiceSpec.scala │ │ │ └── version2/ │ │ │ ├── ApiServiceData.scala │ │ │ └── ApiServiceSpec.scala │ │ └── com/ │ │ └── fortysevendeg/ │ │ └── BaseTestSupport.scala │ ├── app/ │ │ ├── build.sbt │ │ ├── crashlytics/ │ │ │ └── templates/ │ │ │ └── CrashlyticsManifest.xml │ │ ├── project/ │ │ │ └── plugins.sbt │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── cards/ │ │ │ │ └── nine/ │ │ │ │ └── utils/ │ │ │ │ └── SystemBarTintManager.java │ │ │ ├── res/ │ │ │ │ ├── anim/ │ │ │ │ │ ├── elevation_transition.xml │ │ │ │ │ ├── grid_cards_layout_animation.xml │ │ │ │ │ ├── list_slide_in_bottom_animation.xml │ │ │ │ │ └── slide_in_bottom.xml │ │ │ │ ├── color/ │ │ │ │ │ └── wizard_text_button.xml │ │ │ │ ├── drawable/ │ │ │ │ │ ├── background_icon_collection_detail.xml │ │ │ │ │ ├── background_title_fab_menu_item.xml │ │ │ │ │ ├── background_title_fab_menu_item_default.xml │ │ │ │ │ ├── background_title_fab_menu_item_pressed.xml │ │ │ │ │ ├── drawer_pager.xml │ │ │ │ │ ├── drawer_pager_current.xml │ │ │ │ │ ├── drawer_pager_default.xml │ │ │ │ │ ├── fastscroller_bar.xml │ │ │ │ │ ├── fastscroller_signal.xml │ │ │ │ │ ├── mark_widget_resizing.xml │ │ │ │ │ ├── publish_collection_wizard_pager.xml │ │ │ │ │ ├── publish_collection_wizard_pager_current.xml │ │ │ │ │ ├── stroke_widget_selected.xml │ │ │ │ │ ├── wizard_inline_pager.xml │ │ │ │ │ ├── wizard_inline_pager_current.xml │ │ │ │ │ ├── wizard_inline_pager_default.xml │ │ │ │ │ ├── wizard_pager.xml │ │ │ │ │ ├── wizard_pager_current.xml │ │ │ │ │ ├── wizard_pager_default.xml │ │ │ │ │ └── workspaces_pager.xml │ │ │ │ ├── layout/ │ │ │ │ │ ├── about_header_preference.xml │ │ │ │ │ ├── about_team_preference.xml │ │ │ │ │ ├── add_moment_item.xml │ │ │ │ │ ├── app_drawer_layout.xml │ │ │ │ │ ├── app_drawer_panel.xml │ │ │ │ │ ├── app_item.xml │ │ │ │ │ ├── app_link_dialog_activity.xml │ │ │ │ │ ├── app_select_item.xml │ │ │ │ │ ├── apps_moment_layout.xml │ │ │ │ │ ├── base_action_fragment.xml │ │ │ │ │ ├── card_item.xml │ │ │ │ │ ├── collection_bar_view_panel.xml │ │ │ │ │ ├── collection_checkbox.xml │ │ │ │ │ ├── collection_detail_fragment.xml │ │ │ │ │ ├── collection_item.xml │ │ │ │ │ ├── collections_actions_view_panel.xml │ │ │ │ │ ├── collections_detail_activity.xml │ │ │ │ │ ├── collections_detail_tab.xml │ │ │ │ │ ├── collections_workspace_layout.xml │ │ │ │ │ ├── color_info_item_dialog.xml │ │ │ │ │ ├── contact_info_email_dialog.xml │ │ │ │ │ ├── contact_info_general_dialog.xml │ │ │ │ │ ├── contact_info_header.xml │ │ │ │ │ ├── contact_info_phone_dialog.xml │ │ │ │ │ ├── contact_item.xml │ │ │ │ │ ├── dialog_edit_card.xml │ │ │ │ │ ├── dialog_edit_text.xml │ │ │ │ │ ├── edit_moment.xml │ │ │ │ │ ├── edit_moment_hour_layout.xml │ │ │ │ │ ├── edit_moment_wifi_layout.xml │ │ │ │ │ ├── empty_profile_item.xml │ │ │ │ │ ├── fab_item.xml │ │ │ │ │ ├── fastscroller.xml │ │ │ │ │ ├── header_list_item.xml │ │ │ │ │ ├── icon_info_item_dialog.xml │ │ │ │ │ ├── last_call_item.xml │ │ │ │ │ ├── launcher_activity.xml │ │ │ │ │ ├── list_action_apps_fragment.xml │ │ │ │ │ ├── list_action_fragment.xml │ │ │ │ │ ├── list_action_with_scroller_fragment.xml │ │ │ │ │ ├── list_item_popup_menu.xml │ │ │ │ │ ├── menu_header.xml │ │ │ │ │ ├── moment_bar_view_panel.xml │ │ │ │ │ ├── new_collection.xml │ │ │ │ │ ├── private_collections_item.xml │ │ │ │ │ ├── profile_account_item.xml │ │ │ │ │ ├── profile_account_item_header.xml │ │ │ │ │ ├── profile_activity.xml │ │ │ │ │ ├── profile_subscription_item.xml │ │ │ │ │ ├── public_collections_item.xml │ │ │ │ │ ├── publish_collection_wizard.xml │ │ │ │ │ ├── recommendations_item.xml │ │ │ │ │ ├── search_box_panel.xml │ │ │ │ │ ├── search_item.xml │ │ │ │ │ ├── select_collection_dialog.xml │ │ │ │ │ ├── select_collection_item.xml │ │ │ │ │ ├── select_moment_dialog.xml │ │ │ │ │ ├── select_moment_item.xml │ │ │ │ │ ├── shortcut_item.xml │ │ │ │ │ ├── swipe_animation_drawer_layout.xml │ │ │ │ │ ├── tab_item.xml │ │ │ │ │ ├── toolbar_dialog.xml │ │ │ │ │ ├── widget_item.xml │ │ │ │ │ ├── widgets_action_fragment.xml │ │ │ │ │ ├── wizard_activity.xml │ │ │ │ │ ├── wizard_checkbox.xml │ │ │ │ │ ├── wizard_inline.xml │ │ │ │ │ ├── wizard_inline_step.xml │ │ │ │ │ ├── wizard_moment_checkbox.xml │ │ │ │ │ ├── wizard_new_conf_step_0.xml │ │ │ │ │ ├── wizard_new_conf_step_1.xml │ │ │ │ │ ├── wizard_new_conf_step_2.xml │ │ │ │ │ ├── wizard_new_conf_step_3.xml │ │ │ │ │ ├── wizard_new_conf_step_4.xml │ │ │ │ │ ├── wizard_new_conf_step_5.xml │ │ │ │ │ ├── wizard_step.xml │ │ │ │ │ ├── wizard_wifi_checkbox.xml │ │ │ │ │ ├── workspace_button.xml │ │ │ │ │ ├── workspace_item_menu.xml │ │ │ │ │ └── workspace_menu_layout.xml │ │ │ │ ├── menu/ │ │ │ │ │ ├── app_menu.xml │ │ │ │ │ ├── collection_detail_menu.xml │ │ │ │ │ ├── collection_edit_menu.xml │ │ │ │ │ └── profile_menu.xml │ │ │ │ ├── values/ │ │ │ │ │ ├── arrays.xml │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── config.xml │ │ │ │ │ ├── dimens.xml │ │ │ │ │ ├── ids.xml │ │ │ │ │ ├── strings.xml │ │ │ │ │ ├── styles_actions.xml │ │ │ │ │ ├── styles_cards.xml │ │ │ │ │ ├── styles_collections.xml │ │ │ │ │ ├── styles_dialogs.xml │ │ │ │ │ ├── styles_drawer.xml │ │ │ │ │ ├── styles_fast_scroller.xml │ │ │ │ │ ├── styles_items.xml │ │ │ │ │ ├── styles_launcher.xml │ │ │ │ │ ├── styles_menu.xml │ │ │ │ │ ├── styles_menu_items.xml │ │ │ │ │ ├── styles_preferences.xml │ │ │ │ │ ├── styles_profile.xml │ │ │ │ │ ├── styles_pull_to_tabs.xml │ │ │ │ │ ├── styles_wizard.xml │ │ │ │ │ └── themes.xml │ │ │ │ ├── values-es/ │ │ │ │ │ ├── arrays.xml │ │ │ │ │ └── strings.xml │ │ │ │ ├── values-sw400dp/ │ │ │ │ │ ├── dimens.xml │ │ │ │ │ └── strings.xml │ │ │ │ ├── values-sw600dp/ │ │ │ │ │ ├── dimens.xml │ │ │ │ │ └── strings.xml │ │ │ │ ├── values-v17/ │ │ │ │ │ └── dimens.xml │ │ │ │ ├── values-v19/ │ │ │ │ │ └── themes.xml │ │ │ │ ├── values-v21/ │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── dimens.xml │ │ │ │ │ ├── styles_actions.xml │ │ │ │ │ ├── styles_collections.xml │ │ │ │ │ ├── styles_fast_scroller.xml │ │ │ │ │ ├── styles_profile.xml │ │ │ │ │ └── themes.xml │ │ │ │ └── xml/ │ │ │ │ ├── preferences_about.xml │ │ │ │ ├── preferences_analytics.xml │ │ │ │ ├── preferences_animations.xml │ │ │ │ ├── preferences_app_drawer.xml │ │ │ │ ├── preferences_apps_list.xml │ │ │ │ ├── preferences_dev.xml │ │ │ │ ├── preferences_devs_headers.xml │ │ │ │ ├── preferences_headers.xml │ │ │ │ ├── preferences_lookfeel.xml │ │ │ │ └── preferences_moments.xml │ │ │ └── scala/ │ │ │ └── cards/ │ │ │ └── nine/ │ │ │ ├── app/ │ │ │ │ ├── NineCardsApplication.scala │ │ │ │ ├── commons/ │ │ │ │ │ ├── BroadcastDispatcher.scala │ │ │ │ │ ├── ContextSupportPreferences.scala │ │ │ │ │ ├── ContextSupportProvider.scala │ │ │ │ │ └── Conversions.scala │ │ │ │ ├── di/ │ │ │ │ │ └── Injector.scala │ │ │ │ ├── observers/ │ │ │ │ │ ├── NineCardsObserver.scala │ │ │ │ │ └── ObserverRegister.scala │ │ │ │ ├── permissions/ │ │ │ │ │ ├── PermissionChecker.scala │ │ │ │ │ └── PermissionCheckerException.scala │ │ │ │ ├── receivers/ │ │ │ │ │ ├── apps/ │ │ │ │ │ │ ├── AppBroadcastJobs.scala │ │ │ │ │ │ └── AppBroadcastReceiver.scala │ │ │ │ │ ├── bluetooth/ │ │ │ │ │ │ ├── BluetoothJobs.scala │ │ │ │ │ │ └── BluetoothReceiver.scala │ │ │ │ │ ├── moments/ │ │ │ │ │ │ ├── ConnectionStatusChangedJobs.scala │ │ │ │ │ │ └── MomentBroadcastReceiver.scala │ │ │ │ │ └── shortcuts/ │ │ │ │ │ ├── ShortcutBroadcastJobs.scala │ │ │ │ │ └── ShortcutBroadcastReceiver.scala │ │ │ │ ├── services/ │ │ │ │ │ ├── NineCardsFirebaseInstanceIdService.scala │ │ │ │ │ ├── NineCardsFirebaseJobs.scala │ │ │ │ │ ├── NineCardsFirebaseMessagingService.scala │ │ │ │ │ ├── sharedcollections/ │ │ │ │ │ │ ├── UpdateSharedCollectionJobs.scala │ │ │ │ │ │ ├── UpdateSharedCollectionService.scala │ │ │ │ │ │ └── UpdateSharedCollectionUiActions.scala │ │ │ │ │ └── sync/ │ │ │ │ │ ├── SynchronizeDeviceService.scala │ │ │ │ │ └── SynchronizeDeviceServiceJobs.scala │ │ │ │ └── ui/ │ │ │ │ ├── applinks/ │ │ │ │ │ ├── AppLinksReceiverActivity.scala │ │ │ │ │ ├── AppLinksReceiverJobs.scala │ │ │ │ │ └── AppLinksReceiverUiActions.scala │ │ │ │ ├── collections/ │ │ │ │ │ ├── CollectionAdapter.scala │ │ │ │ │ ├── CollectionFragment.scala │ │ │ │ │ ├── CollectionsDetailsActivity.scala │ │ │ │ │ ├── CollectionsPagerAdapter.scala │ │ │ │ │ ├── dialog/ │ │ │ │ │ │ ├── EditCardDialogFragment.scala │ │ │ │ │ │ └── publishcollection/ │ │ │ │ │ │ ├── PublishCollectionActions.scala │ │ │ │ │ │ ├── PublishCollectionDOM.scala │ │ │ │ │ │ ├── PublishCollectionFragment.scala │ │ │ │ │ │ ├── PublishCollectionJobs.scala │ │ │ │ │ │ └── PublishCollectionStyles.scala │ │ │ │ │ ├── jobs/ │ │ │ │ │ │ ├── GroupCollectionsJobs.scala │ │ │ │ │ │ ├── NavigationJobs.scala │ │ │ │ │ │ ├── SharedCollectionJobs.scala │ │ │ │ │ │ ├── SingleCollectionJobs.scala │ │ │ │ │ │ ├── ToolbarJobs.scala │ │ │ │ │ │ └── uiactions/ │ │ │ │ │ │ ├── GroupCollectionsDOM.scala │ │ │ │ │ │ ├── GroupCollectionsUiActions.scala │ │ │ │ │ │ ├── NavigationUiActions.scala │ │ │ │ │ │ ├── SharedCollectionUiActions.scala │ │ │ │ │ │ ├── SingleCollectionDOM.scala │ │ │ │ │ │ ├── SingleCollectionUiActions.scala │ │ │ │ │ │ └── ToolbarUiActions.scala │ │ │ │ │ ├── snails/ │ │ │ │ │ │ └── CollectionsSnails.scala │ │ │ │ │ └── tasks/ │ │ │ │ │ └── CollectionJobs.scala │ │ │ │ ├── commons/ │ │ │ │ │ ├── ActivityFindViews.scala │ │ │ │ │ ├── AppLog.scala │ │ │ │ │ ├── AsyncImageTweaks.scala │ │ │ │ │ ├── Commons.scala │ │ │ │ │ ├── CommonsExcerpt.scala │ │ │ │ │ ├── CommonsTweak.scala │ │ │ │ │ ├── Exceptions.scala │ │ │ │ │ ├── Jobs.scala │ │ │ │ │ ├── SnailsCommons.scala │ │ │ │ │ ├── SystemBarsTint.scala │ │ │ │ │ ├── UiContext.scala │ │ │ │ │ ├── UiExtensions.scala │ │ │ │ │ ├── action_filters/ │ │ │ │ │ │ ├── AppsActionFilter.scala │ │ │ │ │ │ ├── CollectionsActionFilter.scala │ │ │ │ │ │ ├── MomentsActionFilter.scala │ │ │ │ │ │ └── SyncActionFilter.scala │ │ │ │ │ ├── adapters/ │ │ │ │ │ │ ├── apps/ │ │ │ │ │ │ │ ├── AppsAdapter.scala │ │ │ │ │ │ │ └── AppsSelectionAdapter.scala │ │ │ │ │ │ ├── contacts/ │ │ │ │ │ │ │ ├── ContactsAdapter.scala │ │ │ │ │ │ │ └── LastCallsAdapter.scala │ │ │ │ │ │ ├── search/ │ │ │ │ │ │ │ └── SearchAdapter.scala │ │ │ │ │ │ └── sharedcollections/ │ │ │ │ │ │ ├── SharedCollectionItem.scala │ │ │ │ │ │ ├── SharedCollectionsAdapter.scala │ │ │ │ │ │ └── ViewHolderSharedCollectionsLayoutAdapter.scala │ │ │ │ │ ├── dialogs/ │ │ │ │ │ │ ├── BaseActionFragment.scala │ │ │ │ │ │ ├── Styles.scala │ │ │ │ │ │ ├── addmoment/ │ │ │ │ │ │ │ ├── AddMomentAdapter.scala │ │ │ │ │ │ │ ├── AddMomentDOM.scala │ │ │ │ │ │ │ ├── AddMomentFragment.scala │ │ │ │ │ │ │ ├── AddMomentItemDecoration.scala │ │ │ │ │ │ │ ├── AddMomentJobs.scala │ │ │ │ │ │ │ └── AddMomentUiActions.scala │ │ │ │ │ │ ├── apps/ │ │ │ │ │ │ │ ├── AppsDOM.scala │ │ │ │ │ │ │ ├── AppsFragment.scala │ │ │ │ │ │ │ ├── AppsJobs.scala │ │ │ │ │ │ │ └── AppsUiActions.scala │ │ │ │ │ │ ├── contacts/ │ │ │ │ │ │ │ ├── ContactsDOM.scala │ │ │ │ │ │ │ ├── ContactsFragment.scala │ │ │ │ │ │ │ ├── ContactsJobs.scala │ │ │ │ │ │ │ ├── ContactsUiActions.scala │ │ │ │ │ │ │ └── SelectInfoContactDialogFragment.scala │ │ │ │ │ │ ├── createoreditcollection/ │ │ │ │ │ │ │ ├── ColorDialogFragment.scala │ │ │ │ │ │ │ ├── CreateOrEditCollectionDOM.scala │ │ │ │ │ │ │ ├── CreateOrEditCollectionFragment.scala │ │ │ │ │ │ │ ├── CreateOrEditCollectionJobs.scala │ │ │ │ │ │ │ ├── CreateOrEditCollectionUiActions.scala │ │ │ │ │ │ │ └── IconDialogFragment.scala │ │ │ │ │ │ ├── editmoment/ │ │ │ │ │ │ │ ├── EditMomentDOM.scala │ │ │ │ │ │ │ ├── EditMomentFragment.scala │ │ │ │ │ │ │ ├── EditMomentJobs.scala │ │ │ │ │ │ │ └── EditMomentUiActions.scala │ │ │ │ │ │ ├── privatecollections/ │ │ │ │ │ │ │ ├── PrivateCollectionsAdapter.scala │ │ │ │ │ │ │ ├── PrivateCollectionsDOM.scala │ │ │ │ │ │ │ ├── PrivateCollectionsFragment.scala │ │ │ │ │ │ │ ├── PrivateCollectionsJobs.scala │ │ │ │ │ │ │ └── PrivateCollectionsUiActions.scala │ │ │ │ │ │ ├── publicollections/ │ │ │ │ │ │ │ ├── PublicCollectionsDOM.scala │ │ │ │ │ │ │ ├── PublicCollectionsFragment.scala │ │ │ │ │ │ │ ├── PublicCollectionsJobs.scala │ │ │ │ │ │ │ └── PublicCollectionsUiActions.scala │ │ │ │ │ │ ├── recommendations/ │ │ │ │ │ │ │ ├── RecommendationsAdapter.scala │ │ │ │ │ │ │ ├── RecommendationsDOM.scala │ │ │ │ │ │ │ ├── RecommendationsFragment.scala │ │ │ │ │ │ │ ├── RecommendationsJobs.scala │ │ │ │ │ │ │ └── RecommendationsUiActions.scala │ │ │ │ │ │ ├── shortcuts/ │ │ │ │ │ │ │ ├── ShortcutDialogAdapter.scala │ │ │ │ │ │ │ ├── ShortcutDialogDOM.scala │ │ │ │ │ │ │ ├── ShortcutDialogFragment.scala │ │ │ │ │ │ │ ├── ShortcutDialogJobs.scala │ │ │ │ │ │ │ └── ShortcutDialogUiActions.scala │ │ │ │ │ │ ├── widgets/ │ │ │ │ │ │ │ ├── WidgetsAdapter.scala │ │ │ │ │ │ │ ├── WidgetsDialogDOM.scala │ │ │ │ │ │ │ ├── WidgetsDialogJobs.scala │ │ │ │ │ │ │ ├── WidgetsDialogUiActions.scala │ │ │ │ │ │ │ └── WidgetsFragment.scala │ │ │ │ │ │ └── wizard/ │ │ │ │ │ │ ├── WizardInlineDOM.scala │ │ │ │ │ │ ├── WizardInlineFragment.scala │ │ │ │ │ │ ├── WizardInlinePreferences.scala │ │ │ │ │ │ ├── WizardInlineType.scala │ │ │ │ │ │ └── WizardInlineUiActions.scala │ │ │ │ │ ├── glide/ │ │ │ │ │ │ ├── AppIconLoader.scala │ │ │ │ │ │ ├── ApplicationIconDecoder.scala │ │ │ │ │ │ ├── IconFromPackageDecoder.scala │ │ │ │ │ │ └── IconFromPackageLoader.scala │ │ │ │ │ ├── google_api/ │ │ │ │ │ │ └── GoogleApiClientProvider.scala │ │ │ │ │ ├── ops/ │ │ │ │ │ │ ├── CollectionOps.scala │ │ │ │ │ │ ├── ConditionWeatherOps.scala │ │ │ │ │ │ ├── DrawableOps.scala │ │ │ │ │ │ ├── NineCardsCategoryOps.scala │ │ │ │ │ │ ├── NineCardsMomentOps.scala │ │ │ │ │ │ ├── ResourcesCollectionDataOps.scala │ │ │ │ │ │ ├── SharedCollectionOps.scala │ │ │ │ │ │ ├── SubscriptionOps.scala │ │ │ │ │ │ ├── TaskServiceOps.scala │ │ │ │ │ │ ├── TryOps.scala │ │ │ │ │ │ ├── UiOps.scala │ │ │ │ │ │ ├── ViewGroupOps.scala │ │ │ │ │ │ ├── ViewOps.scala │ │ │ │ │ │ └── WidgetsOps.scala │ │ │ │ │ ├── states/ │ │ │ │ │ │ └── MomentState.scala │ │ │ │ │ └── styles/ │ │ │ │ │ ├── CollectionCardsStyles.scala │ │ │ │ │ └── CommonStyles.scala │ │ │ │ ├── components/ │ │ │ │ │ ├── adapters/ │ │ │ │ │ │ └── ThemeArrayAdapter.scala │ │ │ │ │ ├── commons/ │ │ │ │ │ │ ├── PaddingItemDecoration.scala │ │ │ │ │ │ ├── ReorderItemTouchHelperCallback.scala │ │ │ │ │ │ ├── SelectedItemDecoration.scala │ │ │ │ │ │ ├── SwipeController.scala │ │ │ │ │ │ ├── TranslationAnimator.scala │ │ │ │ │ │ └── ViewState.scala │ │ │ │ │ ├── dialogs/ │ │ │ │ │ │ ├── AlertDialogFragment.scala │ │ │ │ │ │ ├── BluetoothDialogFragment.scala │ │ │ │ │ │ ├── CollectionDialog.scala │ │ │ │ │ │ ├── MomentDialog.scala │ │ │ │ │ │ └── WifiDialogFragment.scala │ │ │ │ │ ├── drawables/ │ │ │ │ │ │ ├── BackgroundSelectedDrawable.scala │ │ │ │ │ │ ├── CharDrawable.scala │ │ │ │ │ │ ├── DottedDrawable.scala │ │ │ │ │ │ ├── DrawerAnimationBackgroundDrawable.scala │ │ │ │ │ │ ├── DrawerBackgroundDrawable.scala │ │ │ │ │ │ ├── DropBackgroundDrawable.scala │ │ │ │ │ │ ├── EdgeWorkspaceDrawable.scala │ │ │ │ │ │ ├── PathMorphDrawable.scala │ │ │ │ │ │ ├── RippleCollectionDrawable.scala │ │ │ │ │ │ ├── TopBarMomentBackgroundDrawable.scala │ │ │ │ │ │ ├── TopBarMomentEdgeBackgroundDrawable.scala │ │ │ │ │ │ └── tweaks/ │ │ │ │ │ │ └── PathMorphDrawableTweaks.scala │ │ │ │ │ ├── layouts/ │ │ │ │ │ │ ├── AnimatedWorkSpaces.scala │ │ │ │ │ │ ├── AppsMomentLayout.scala │ │ │ │ │ │ ├── CollectionActionsPanelLayout.scala │ │ │ │ │ │ ├── DialogToolbar.scala │ │ │ │ │ │ ├── DockAppsPanelLayout.scala │ │ │ │ │ │ ├── EditDeviceMomentLayout.scala │ │ │ │ │ │ ├── EditHourMomentLayout.scala │ │ │ │ │ │ ├── FabItemMenu.scala │ │ │ │ │ │ ├── FastScrollerLayout.scala │ │ │ │ │ │ ├── LauncherWorkSpaces.scala │ │ │ │ │ │ ├── PullToCloseView.scala │ │ │ │ │ │ ├── PullToDownView.scala │ │ │ │ │ │ ├── PullToTabsView.scala │ │ │ │ │ │ ├── SearchBoxView.scala │ │ │ │ │ │ ├── SlidingTabLayout.scala │ │ │ │ │ │ ├── StepsWorkspaces.scala │ │ │ │ │ │ ├── TopBarLayout.scala │ │ │ │ │ │ ├── WizardInlineWorkspaces.scala │ │ │ │ │ │ ├── WorkSpaceButton.scala │ │ │ │ │ │ ├── WorkspaceItemMenu.scala │ │ │ │ │ │ ├── snails/ │ │ │ │ │ │ │ └── LayoutSnails.scala │ │ │ │ │ │ └── tweaks/ │ │ │ │ │ │ └── LayoutsTweaks.scala │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── LauncherData.scala │ │ │ │ │ ├── preferences/ │ │ │ │ │ │ ├── AboutHeaderPreference.scala │ │ │ │ │ │ └── TeamPreference.scala │ │ │ │ │ └── widgets/ │ │ │ │ │ ├── CollectionCheckBox.scala │ │ │ │ │ ├── CollectionRecyclerView.scala │ │ │ │ │ ├── DrawerRecyclerView.scala │ │ │ │ │ ├── LauncherNoConfiguredWidgetView.scala │ │ │ │ │ ├── LauncherWidgetResizeFrame.scala │ │ │ │ │ ├── LauncherWidgetView.scala │ │ │ │ │ ├── ScrollingLinearLayoutManager.scala │ │ │ │ │ ├── TintableButton.scala │ │ │ │ │ ├── TintableImageView.scala │ │ │ │ │ ├── WizardCheckBox.scala │ │ │ │ │ ├── WizardMomentCheckBox.scala │ │ │ │ │ ├── WizardWifiCheckBox.scala │ │ │ │ │ └── tweaks/ │ │ │ │ │ └── WidgetsTweaks.scala │ │ │ │ ├── launcher/ │ │ │ │ │ ├── LauncherActivity.scala │ │ │ │ │ ├── exceptions/ │ │ │ │ │ │ └── LauncherExceptions.scala │ │ │ │ │ ├── holders/ │ │ │ │ │ │ ├── LauncherWorkSpaceCollectionsHolder.scala │ │ │ │ │ │ └── LauncherWorkSpaceMomentsHolder.scala │ │ │ │ │ ├── jobs/ │ │ │ │ │ │ ├── AppDrawerJobs.scala │ │ │ │ │ │ ├── DragJobs.scala │ │ │ │ │ │ ├── LauncherJobs.scala │ │ │ │ │ │ ├── NavigationJobs.scala │ │ │ │ │ │ ├── WidgetsJobs.scala │ │ │ │ │ │ └── uiactions/ │ │ │ │ │ │ ├── AppDrawerUiActions.scala │ │ │ │ │ │ ├── DockAppsUiActions.scala │ │ │ │ │ │ ├── DragUiActions.scala │ │ │ │ │ │ ├── LauncherDOM.scala │ │ │ │ │ │ ├── LauncherUiActions.scala │ │ │ │ │ │ ├── MenuDrawersUiActions.scala │ │ │ │ │ │ ├── NavigationUiActions.scala │ │ │ │ │ │ ├── TopBarUiActions.scala │ │ │ │ │ │ ├── WidgetUiActions.scala │ │ │ │ │ │ └── WorkspaceUiActions.scala │ │ │ │ │ ├── snails/ │ │ │ │ │ │ ├── DrawerSnails.scala │ │ │ │ │ │ └── LauncherSnails.scala │ │ │ │ │ └── types/ │ │ │ │ │ ├── AppDrawerIconShadowBuilder.scala │ │ │ │ │ ├── CollectionShadowBuilder.scala │ │ │ │ │ ├── DragLauncherType.scala │ │ │ │ │ ├── DragObject.scala │ │ │ │ │ ├── MenuOptions.scala │ │ │ │ │ └── WidgetShadowBuilder.scala │ │ │ │ ├── preferences/ │ │ │ │ │ ├── NineCardsPreferencesActivity.scala │ │ │ │ │ ├── PreferencesJobs.scala │ │ │ │ │ ├── PreferencesUiActions.scala │ │ │ │ │ ├── about/ │ │ │ │ │ │ └── AboutFragment.scala │ │ │ │ │ ├── analytics/ │ │ │ │ │ │ └── AnalyticsFragment.scala │ │ │ │ │ ├── animations/ │ │ │ │ │ │ └── AnimationsFragment.scala │ │ │ │ │ ├── appdrawer/ │ │ │ │ │ │ ├── AppDrawerFragment.scala │ │ │ │ │ │ ├── AppDrawerJobs.scala │ │ │ │ │ │ └── AppDrawerUiActions.scala │ │ │ │ │ ├── commons/ │ │ │ │ │ │ ├── FindPreferences.scala │ │ │ │ │ │ ├── NineCardsPreferences.scala │ │ │ │ │ │ ├── NineCardsPreferencesValues.scala │ │ │ │ │ │ └── PreferenceChangeListenerFragment.scala │ │ │ │ │ ├── developers/ │ │ │ │ │ │ ├── AppsListFragment.scala │ │ │ │ │ │ ├── AppsListJobs.scala │ │ │ │ │ │ ├── AppsListUiActions.scala │ │ │ │ │ │ ├── DeveloperFragment.scala │ │ │ │ │ │ ├── DeveloperJobs.scala │ │ │ │ │ │ └── DeveloperUiActions.scala │ │ │ │ │ ├── lookandfeel/ │ │ │ │ │ │ └── LookFeelFragment.scala │ │ │ │ │ └── moments/ │ │ │ │ │ └── MomentsFragment.scala │ │ │ │ ├── profile/ │ │ │ │ │ ├── ProfileActivity.scala │ │ │ │ │ ├── adapters/ │ │ │ │ │ │ ├── AccountsAdapter.scala │ │ │ │ │ │ ├── EmptyProfileAdapter.scala │ │ │ │ │ │ └── SubscriptionsAdapter.scala │ │ │ │ │ ├── dialog/ │ │ │ │ │ │ ├── EditAccountDeviceDialogFragment.scala │ │ │ │ │ │ └── RemoveAccountDeviceDialogFragment.scala │ │ │ │ │ ├── jobs/ │ │ │ │ │ │ ├── ProfileDOM.scala │ │ │ │ │ │ ├── ProfileJobs.scala │ │ │ │ │ │ └── ProfileUiActions.scala │ │ │ │ │ └── models/ │ │ │ │ │ └── Model.scala │ │ │ │ ├── share/ │ │ │ │ │ ├── SharedContentActivity.scala │ │ │ │ │ ├── SharedContentJobs.scala │ │ │ │ │ ├── SharedContentUiActions.scala │ │ │ │ │ └── models/ │ │ │ │ │ └── Models.scala │ │ │ │ └── wizard/ │ │ │ │ ├── WizardActivity.scala │ │ │ │ ├── WizardExceptions.scala │ │ │ │ ├── jobs/ │ │ │ │ │ ├── LoadConfigurationJobs.scala │ │ │ │ │ ├── NewConfigurationJobs.scala │ │ │ │ │ ├── WizardJobs.scala │ │ │ │ │ └── uiactions/ │ │ │ │ │ ├── NewConfigurationUiActions.scala │ │ │ │ │ ├── VisibilityUiActions.scala │ │ │ │ │ ├── WizardDOM.scala │ │ │ │ │ └── WizardUiActions.scala │ │ │ │ └── models/ │ │ │ │ └── Models.scala │ │ │ ├── process/ │ │ │ │ ├── cloud/ │ │ │ │ │ ├── CloudStorageProcess.scala │ │ │ │ │ ├── Conversions.scala │ │ │ │ │ ├── Exceptions.scala │ │ │ │ │ ├── impl/ │ │ │ │ │ │ └── CloudStorageProcessImpl.scala │ │ │ │ │ └── models/ │ │ │ │ │ └── CloudStorageImplicits.scala │ │ │ │ ├── commons/ │ │ │ │ │ └── Models.scala │ │ │ │ ├── social/ │ │ │ │ │ ├── Exceptions.scala │ │ │ │ │ ├── SocialProfileProcess.scala │ │ │ │ │ └── impl/ │ │ │ │ │ └── SocialProfileProcessImpl.scala │ │ │ │ └── thirdparty/ │ │ │ │ ├── Exceptions.scala │ │ │ │ └── ExternalServicesProcess.scala │ │ │ └── services/ │ │ │ ├── analytics/ │ │ │ │ └── impl/ │ │ │ │ └── AnalyticsTrackServices.scala │ │ │ ├── awareness/ │ │ │ │ └── impl/ │ │ │ │ ├── GoogleAwarenessServicesImpl.scala │ │ │ │ └── Models.scala │ │ │ ├── drive/ │ │ │ │ ├── Conversions.scala │ │ │ │ ├── DriveServices.scala │ │ │ │ ├── Exceptions.scala │ │ │ │ ├── impl/ │ │ │ │ │ └── DriveServicesImpl.scala │ │ │ │ └── models/ │ │ │ │ └── DriveServicesModels.scala │ │ │ ├── permissions/ │ │ │ │ └── impl/ │ │ │ │ └── AndroidSupportPermissionsServices.scala │ │ │ └── plus/ │ │ │ ├── Exceptions.scala │ │ │ ├── GooglePlusServices.scala │ │ │ ├── impl/ │ │ │ │ └── GooglePlusServicesImpl.scala │ │ │ └── models/ │ │ │ └── Model.scala │ │ └── test/ │ │ └── scala/ │ │ └── cards/ │ │ └── nine/ │ │ ├── app/ │ │ │ ├── observers/ │ │ │ │ └── ObserverRegisterSpecification.scala │ │ │ ├── receivers/ │ │ │ │ └── bluetooth/ │ │ │ │ ├── BluetoothContextSupport.scala │ │ │ │ └── BluetoothJobsSpecification.scala │ │ │ └── ui/ │ │ │ ├── applinks/ │ │ │ │ └── AppLinksReceiverJobsSpec.scala │ │ │ ├── collections/ │ │ │ │ └── jobs/ │ │ │ │ ├── GroupCollectionsJobsSpecification.scala │ │ │ │ ├── NavigationJobsSpecification.scala │ │ │ │ ├── ShareCollectionJobsSpecification.scala │ │ │ │ ├── SingleCollectionJobsSpecification.scala │ │ │ │ └── ToolbarJobsSpecification.scala │ │ │ ├── commons/ │ │ │ │ ├── JobsSpec.scala │ │ │ │ └── dialogs/ │ │ │ │ ├── addMoment/ │ │ │ │ │ └── AddMomentJobsSpec.scala │ │ │ │ ├── apps/ │ │ │ │ │ └── AppsJobsSpec.scala │ │ │ │ ├── contacts/ │ │ │ │ │ └── ContactsJobsSpec.scala │ │ │ │ ├── editmoment/ │ │ │ │ │ └── EditMomentJobsSpec.scala │ │ │ │ ├── privatecollections/ │ │ │ │ │ └── PrivateCollectionsJobsSpec.scala │ │ │ │ ├── publicCollections/ │ │ │ │ │ └── PublicCollectionsJobsJobsSpec.scala │ │ │ │ ├── recommendations/ │ │ │ │ │ └── RecommendationsJobsSpec.scala │ │ │ │ ├── shortcuts/ │ │ │ │ │ └── ShortcutJobsSpec.scala │ │ │ │ └── widgets/ │ │ │ │ └── WidgetsDialogJobsSpec.scala │ │ │ ├── data/ │ │ │ │ └── IterableData.scala │ │ │ ├── launcher/ │ │ │ │ └── jobs/ │ │ │ │ ├── AppDrawerJobsSpecification.scala │ │ │ │ ├── DragJobsSpecification.scala │ │ │ │ ├── LauncherJobsSpecification.scala │ │ │ │ ├── LauncherTestData.scala │ │ │ │ ├── NavigationJobsSpecification.scala │ │ │ │ └── WidgetJobsSpecification.scala │ │ │ ├── profile/ │ │ │ │ └── jobs/ │ │ │ │ └── ProfileJobsSpec.scala │ │ │ └── wizard/ │ │ │ └── jobs/ │ │ │ ├── LoadConfigurationJobsSpec.scala │ │ │ ├── NewConfigurationJobsSpecification.scala │ │ │ └── WizardJobsSpecification.scala │ │ └── process/ │ │ ├── cloud/ │ │ │ └── impl/ │ │ │ ├── CloudStorageProcessImplData.scala │ │ │ └── CloudStorageProcessImplSpecification.scala │ │ └── social/ │ │ └── impl/ │ │ ├── SocialProfileProcessImplData.scala │ │ └── SocialProfileProcessImplSpecification.scala │ ├── commons/ │ │ ├── build.sbt │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── scala/ │ │ │ └── cards/ │ │ │ └── nine/ │ │ │ └── commons/ │ │ │ ├── CatchAll.scala │ │ │ ├── NineCardExtensions.scala │ │ │ ├── contentresolver/ │ │ │ │ ├── ContentResolverWrapper.scala │ │ │ │ ├── Conversions.scala │ │ │ │ ├── NotificationUri.scala │ │ │ │ └── UriCreator.scala │ │ │ ├── contexts/ │ │ │ │ └── ContextSupport.scala │ │ │ ├── ops/ │ │ │ │ ├── ColorOps.scala │ │ │ │ └── SeqOps.scala │ │ │ ├── package.scala │ │ │ ├── services/ │ │ │ │ └── package.scala │ │ │ └── utils/ │ │ │ ├── Exceptions.scala │ │ │ ├── FileUtils.scala │ │ │ ├── StreamWrapper.scala │ │ │ └── impl/ │ │ │ └── StreamWrapperImpl.scala │ │ └── test/ │ │ └── scala/ │ │ └── cards/ │ │ └── nine/ │ │ └── commons/ │ │ └── utils/ │ │ ├── FileUtilsData.scala │ │ ├── FileUtilsSpec.scala │ │ └── impl/ │ │ └── StreamWrapperImplSpec.scala │ ├── commons-tests/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── cards/ │ │ └── nine/ │ │ └── commons/ │ │ └── test/ │ │ ├── TaskServiceTestOps.scala │ │ ├── data/ │ │ │ ├── ApiTestData.scala │ │ │ ├── ApiV1TestData.scala │ │ │ ├── AppWidgetTestData.scala │ │ │ ├── ApplicationTestData.scala │ │ │ ├── CardTestData.scala │ │ │ ├── CloudStorageTestData.scala │ │ │ ├── CollectionTestData.scala │ │ │ ├── Constants.scala │ │ │ ├── DeviceTestData.scala │ │ │ ├── DockAppTestData.scala │ │ │ ├── LauncherExecutorTestData.scala │ │ │ ├── MomentTestData.scala │ │ │ ├── SharedCollectionTestData.scala │ │ │ ├── UserTestData.scala │ │ │ ├── WidgetTestData.scala │ │ │ └── trackevent/ │ │ │ ├── AppDrawerTrackEventTestData.scala │ │ │ ├── CollectionDetailTrackEventTestData.scala │ │ │ ├── HomeTrackEventTestData.scala │ │ │ ├── LauncherTrackEventTestData.scala │ │ │ ├── MomentsTrackEventTestData.scala │ │ │ ├── ProfileTrackEventTestData.scala │ │ │ ├── SliderMenuTrackEventTestData.scala │ │ │ ├── WidgetTrackEventTestData.scala │ │ │ └── WizardTrackEventTestData.scala │ │ └── repository/ │ │ └── MockCursor.scala │ ├── docs/ │ │ └── src/ │ │ └── main/ │ │ ├── resources/ │ │ │ └── microsite/ │ │ │ ├── css/ │ │ │ │ └── custom.css │ │ │ ├── data/ │ │ │ │ └── menu.yml │ │ │ ├── img/ │ │ │ │ ├── gallery1.webm │ │ │ │ ├── gallery2.webm │ │ │ │ └── gallery3.webm │ │ │ ├── includes/ │ │ │ │ ├── ninecards-footer.html │ │ │ │ └── ninecards-header.html │ │ │ └── layouts/ │ │ │ └── ninecards-home.html │ │ └── tut/ │ │ ├── CNAME │ │ ├── docs/ │ │ │ ├── Client.md │ │ │ ├── Libraries.md │ │ │ ├── Server.md │ │ │ ├── client/ │ │ │ │ ├── Architecture.md │ │ │ │ ├── CloudStorage.md │ │ │ │ ├── Database.md │ │ │ │ └── Installation.md │ │ │ ├── index.md │ │ │ └── server/ │ │ │ ├── Architecture.md │ │ │ ├── Authentication.md │ │ │ ├── Cache.md │ │ │ ├── Ednpoints.md │ │ │ └── Installation.md │ │ └── index.md │ ├── mock-android/ │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── android/ │ │ ├── accounts/ │ │ │ ├── Account.java │ │ │ ├── AccountManager.java │ │ │ ├── AccountsException.java │ │ │ └── OperationCanceledException.java │ │ ├── app/ │ │ │ └── Activity.java │ │ ├── content/ │ │ │ ├── ComponentName.java │ │ │ ├── ContentResolver.java │ │ │ ├── Intent.java │ │ │ └── res/ │ │ │ └── AssetManager.java │ │ ├── database/ │ │ │ └── ContentObserver.java │ │ ├── graphics/ │ │ │ ├── Bitmap.java │ │ │ ├── Color.java │ │ │ ├── Rect.java │ │ │ └── drawable/ │ │ │ └── Drawable.java │ │ ├── media/ │ │ │ └── ThumbnailUtils.java │ │ ├── net/ │ │ │ └── wifi/ │ │ │ └── WifiConfiguration.java │ │ ├── os/ │ │ │ ├── Bundle.java │ │ │ ├── Handler.java │ │ │ └── Looper.java │ │ ├── util/ │ │ │ └── Log.java │ │ └── view/ │ │ ├── SearchEvent.java │ │ └── View.java │ ├── models/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── cards/ │ │ └── nine/ │ │ └── models/ │ │ ├── Api.scala │ │ ├── ApiV1.scala │ │ ├── Application.scala │ │ ├── Awareness.scala │ │ ├── BitmapPath.scala │ │ ├── Call.scala │ │ ├── Card.scala │ │ ├── CloudStorage.scala │ │ ├── Collection.scala │ │ ├── CollectionProcessConfig.scala │ │ ├── Contact.scala │ │ ├── Conversions.scala │ │ ├── DockApp.scala │ │ ├── IconResize.scala │ │ ├── Intent.scala │ │ ├── IterableCursor.scala │ │ ├── Iterables.scala │ │ ├── LauncherExecutorProcessConfig.scala │ │ ├── Moment.scala │ │ ├── NineCardsBluetoothDevice.scala │ │ ├── NineCardsIntent.scala │ │ ├── NineCardsIntentConversions.scala │ │ ├── Rank.scala │ │ ├── SharedCollection.scala │ │ ├── ShortCut.scala │ │ ├── TermCounter.scala │ │ ├── Theme.scala │ │ ├── TrackEvent.scala │ │ ├── User.scala │ │ ├── Widget.scala │ │ ├── package.scala │ │ ├── reads/ │ │ │ └── MomentImplicits.scala │ │ └── types/ │ │ ├── Action.scala │ │ ├── AppPermission.scala │ │ ├── AwarenessFenceUpdate.scala │ │ ├── BluetoothType.scala │ │ ├── CallType.scala │ │ ├── CardType.scala │ │ ├── Category.scala │ │ ├── CollectionType.scala │ │ ├── ConditionWeather.scala │ │ ├── ContactsFilter.scala │ │ ├── DialogToolbarType.scala │ │ ├── DockType.scala │ │ ├── EmailCategory.scala │ │ ├── FetchAppOrder.scala │ │ ├── GetAppOrder.scala │ │ ├── KindActivity.scala │ │ ├── Label.scala │ │ ├── NineCardsCategory.scala │ │ ├── NineCardsMoment.scala │ │ ├── Permission.scala │ │ ├── PhoneCategory.scala │ │ ├── PublicCollectionStatus.scala │ │ ├── Screen.scala │ │ ├── TypeSharedCollection.scala │ │ ├── Value.scala │ │ ├── WidgetResizeMode.scala │ │ ├── WidgetType.scala │ │ ├── json/ │ │ │ ├── CollectionTypeImplicits.scala │ │ │ ├── DockTypeImplicits.scala │ │ │ ├── NineCardCategoryImplicits.scala │ │ │ ├── NineCardsMomentImplicits.scala │ │ │ └── WidgetTypeImplicits.scala │ │ └── theme/ │ │ ├── ThemeStyleType.scala │ │ └── ThemeType.scala │ ├── process/ │ │ ├── build.sbt │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── assets/ │ │ │ │ ├── theme_dark.json │ │ │ │ └── theme_light.json │ │ │ └── scala/ │ │ │ └── cards/ │ │ │ └── nine/ │ │ │ └── process/ │ │ │ ├── accounts/ │ │ │ │ ├── Exceptions.scala │ │ │ │ ├── UserAccountsProcess.scala │ │ │ │ └── impl/ │ │ │ │ └── UserAccountsProcessImpl.scala │ │ │ ├── collection/ │ │ │ │ ├── CollectionProcess.scala │ │ │ │ ├── CollectionsExceptions.scala │ │ │ │ └── impl/ │ │ │ │ ├── CardsProcessImpl.scala │ │ │ │ ├── CollectionProcessDependencies.scala │ │ │ │ ├── CollectionProcessImpl.scala │ │ │ │ └── CollectionsProcessImpl.scala │ │ │ ├── device/ │ │ │ │ ├── DeviceExceptions.scala │ │ │ │ ├── DeviceProcess.scala │ │ │ │ └── impl/ │ │ │ │ ├── AppsDeviceProcessImpl.scala │ │ │ │ ├── ContactsDeviceProcessImpl.scala │ │ │ │ ├── DeviceProcessDependencies.scala │ │ │ │ ├── DeviceProcessImpl.scala │ │ │ │ ├── DockAppsDeviceProcessImpl.scala │ │ │ │ ├── LastCallsDeviceProcessImpl.scala │ │ │ │ ├── ResetProcessImpl.scala │ │ │ │ ├── ShortcutsDeviceProcessImpl.scala │ │ │ │ └── WidgetsDeviceProcessImpl.scala │ │ │ ├── intents/ │ │ │ │ ├── Exceptions.scala │ │ │ │ ├── LauncherExecutorProcess.scala │ │ │ │ └── impl/ │ │ │ │ └── LauncherExecutorProcessImpl.scala │ │ │ ├── moment/ │ │ │ │ ├── MomentException.scala │ │ │ │ ├── MomentProcess.scala │ │ │ │ └── impl/ │ │ │ │ └── MomentProcessImpl.scala │ │ │ ├── recognition/ │ │ │ │ ├── Exceptions.scala │ │ │ │ ├── RecognitionProcess.scala │ │ │ │ └── impl/ │ │ │ │ └── RecognitionProcessImpl.scala │ │ │ ├── recommendations/ │ │ │ │ ├── RecommendationsExceptions.scala │ │ │ │ ├── RecommendationsProcess.scala │ │ │ │ └── impl/ │ │ │ │ └── RecommendationsProcessImpl.scala │ │ │ ├── sharedcollections/ │ │ │ │ ├── Exceptions.scala │ │ │ │ ├── SharedCollectionsProcess.scala │ │ │ │ └── impl/ │ │ │ │ └── SharedCollectionsProcessImpl.scala │ │ │ ├── theme/ │ │ │ │ ├── ThemeExceptions.scala │ │ │ │ ├── ThemeProcess.scala │ │ │ │ └── impl/ │ │ │ │ └── ThemeProcessImpl.scala │ │ │ ├── trackevent/ │ │ │ │ ├── Exceptions.scala │ │ │ │ ├── TrackEventProcess.scala │ │ │ │ └── impl/ │ │ │ │ ├── AppDrawerEventProcessImpl.scala │ │ │ │ ├── CollectionDetailTrackEventProcessImpl.scala │ │ │ │ ├── HomeTrackEventProcessImpl.scala │ │ │ │ ├── LauncherTrackEventProcessImpl.scala │ │ │ │ ├── MomentsTrackEventProcessImpl.scala │ │ │ │ ├── ProfileTrackEventProcessImpl.scala │ │ │ │ ├── SliderMenuTrackEventProcessImpl.scala │ │ │ │ ├── TrackEventDependencies.scala │ │ │ │ ├── TrackEventProcessImpl.scala │ │ │ │ ├── WidgetTrackEventProcessImpl.scala │ │ │ │ └── WizardTrackEventProcessImpl.scala │ │ │ ├── user/ │ │ │ │ ├── UserExceptions.scala │ │ │ │ ├── UserProcess.scala │ │ │ │ └── impl/ │ │ │ │ └── UserProcessImpl.scala │ │ │ ├── userv1/ │ │ │ │ ├── UserV1Exceptions.scala │ │ │ │ ├── UserV1Process.scala │ │ │ │ └── impl/ │ │ │ │ └── UserV1ProcessImpl.scala │ │ │ ├── utils/ │ │ │ │ └── ApiUtils.scala │ │ │ └── widget/ │ │ │ ├── AppWidgetException.scala │ │ │ ├── WidgetProcess.scala │ │ │ └── impl/ │ │ │ └── WidgetProcessImpl.scala │ │ └── test/ │ │ └── scala/ │ │ └── cards/ │ │ └── nine/ │ │ └── process/ │ │ ├── accounts/ │ │ │ └── impl/ │ │ │ ├── UserAccountsProcessImplData.scala │ │ │ └── UserAccountsProcessImplSpecification.scala │ │ ├── collection/ │ │ │ └── impl/ │ │ │ └── CollectionProcessImplSpec.scala │ │ ├── device/ │ │ │ └── impl/ │ │ │ ├── DeviceProcessData.scala │ │ │ └── DeviceProcessImplSpec.scala │ │ ├── intents/ │ │ │ └── impl/ │ │ │ └── LauncherExecutorProcessImplSpec.scala │ │ ├── moment/ │ │ │ └── impl/ │ │ │ ├── MomentProcessImplData.scala │ │ │ └── MomentProcessImplSpec.scala │ │ ├── recognition/ │ │ │ └── impl/ │ │ │ ├── RecognitionProcessData.scala │ │ │ └── RecognitionProcessImplSpec.scala │ │ ├── recommendations/ │ │ │ └── impl/ │ │ │ └── RecommendationsProcessSpec.scala │ │ ├── sharedcollections/ │ │ │ └── impl/ │ │ │ └── SharedCollectionsProcessImplSpec.scala │ │ ├── theme/ │ │ │ └── impl/ │ │ │ ├── ThemeProcessData.scala │ │ │ └── ThemeProcessImplSpec.scala │ │ ├── trackevent/ │ │ │ └── impl/ │ │ │ ├── AppDrawerTrackEventProcessImplSpec.scala │ │ │ ├── CollectionDetailTrackEventProcessImplSpec.scala │ │ │ ├── HomeTrackEventProcessImplSpec.scala │ │ │ ├── LauncherTrackEventProcessImplSpec.scala │ │ │ ├── MomentsTrackEventProcessImplSpec.scala │ │ │ ├── ProfileTrackEventProcessImplSpec.scala │ │ │ ├── SliderMenuTrackEventProcessImplSpec.scala │ │ │ ├── WidgetTrackEventProcessImplSpec.scala │ │ │ └── WizardTrackEventProcessImplSpec.scala │ │ ├── user/ │ │ │ └── impl/ │ │ │ └── UserProcessImplSpec.scala │ │ ├── userv1/ │ │ │ └── impl/ │ │ │ └── UserV1ProcessImplSpec.scala │ │ ├── utils/ │ │ │ └── ApiUtilsSpec.scala │ │ └── widget/ │ │ └── impl/ │ │ └── WidgetProcessImplSpec.scala │ ├── repository/ │ │ ├── build.sbt │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── scala/ │ │ │ └── cards/ │ │ │ └── nine/ │ │ │ └── repository/ │ │ │ ├── Conversions.scala │ │ │ ├── Exceptions.scala │ │ │ ├── model/ │ │ │ │ └── Model.scala │ │ │ ├── provider/ │ │ │ │ ├── AppEntity.scala │ │ │ │ ├── CardEntity.scala │ │ │ │ ├── CollectionEntity.scala │ │ │ │ ├── DockAppEntity.scala │ │ │ │ ├── MomentEntity.scala │ │ │ │ ├── NineCardsContentProvider.scala │ │ │ │ ├── NineCardsSqlHelper.scala │ │ │ │ ├── NineCardsUri.scala │ │ │ │ ├── UserEntity.scala │ │ │ │ └── WidgetEntity.scala │ │ │ └── repositories/ │ │ │ ├── AppRepository.scala │ │ │ ├── CardRepository.scala │ │ │ ├── CollectionRepository.scala │ │ │ ├── DockAppRepository.scala │ │ │ ├── MomentRepository.scala │ │ │ ├── RepositoryUtils.scala │ │ │ ├── UserRepository.scala │ │ │ └── WidgetRepository.scala │ │ └── test/ │ │ └── scala/ │ │ └── cards/ │ │ └── nine/ │ │ └── repository/ │ │ ├── app/ │ │ │ ├── AppRepositorySpec.scala │ │ │ └── AppRepositoryTestData.scala │ │ ├── card/ │ │ │ ├── CardRepositorySpec.scala │ │ │ └── CardRepositoryTestData.scala │ │ ├── collection/ │ │ │ ├── CollectionRepositorySpec.scala │ │ │ └── CollectionRepositoryTestData.scala │ │ ├── dockapp/ │ │ │ ├── DockAppRepositorySpec.scala │ │ │ └── DockAppRepositoryTestData.scala │ │ ├── moment/ │ │ │ ├── MomentRepositorySpec.scala │ │ │ └── MomentRepositoryTestData.scala │ │ ├── user/ │ │ │ ├── UserRepositorySpec.scala │ │ │ └── UserRepositoryTestData.scala │ │ └── widget/ │ │ ├── WidgetRepositorySpec.scala │ │ └── WidgetRepositoryTestData.scala │ └── services/ │ ├── build.sbt │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── cards/ │ │ │ └── nine/ │ │ │ └── services/ │ │ │ └── contacts/ │ │ │ └── Fields.java │ │ └── scala/ │ │ └── cards/ │ │ └── nine/ │ │ └── services/ │ │ ├── api/ │ │ │ ├── ApiServices.scala │ │ │ ├── Conversions.scala │ │ │ ├── Exceptions.scala │ │ │ └── impl/ │ │ │ └── ApiServicesImpl.scala │ │ ├── apps/ │ │ │ ├── AppsServices.scala │ │ │ ├── Exceptions.scala │ │ │ └── impl/ │ │ │ └── AppsServicesImpl.scala │ │ ├── awareness/ │ │ │ ├── AwarenessServices.scala │ │ │ └── Exceptions.scala │ │ ├── calls/ │ │ │ ├── CallsContentProvider.scala │ │ │ ├── CallsServices.scala │ │ │ ├── Exceptions.scala │ │ │ └── impl/ │ │ │ └── CallsServicesImpl.scala │ │ ├── connectivity/ │ │ │ ├── ConnectivityServices.scala │ │ │ ├── Exceptions.scala │ │ │ └── impl/ │ │ │ └── ConnectivityServicesImpl.scala │ │ ├── contacts/ │ │ │ ├── ContactsContentProvider.scala │ │ │ ├── ContactsServices.scala │ │ │ ├── Exceptions.scala │ │ │ └── impl/ │ │ │ └── ContactsServicesImpl.scala │ │ ├── image/ │ │ │ ├── Exceptions.scala │ │ │ ├── ImageServices.scala │ │ │ └── impl/ │ │ │ ├── ImageServicesImpl.scala │ │ │ └── ImageServicesTasks.scala │ │ ├── intents/ │ │ │ ├── Exceptions.scala │ │ │ ├── LauncherIntentServices.scala │ │ │ └── impl/ │ │ │ ├── IntentCreator.scala │ │ │ └── LauncherIntentServicesImpl.scala │ │ ├── permissions/ │ │ │ ├── Exceptions.scala │ │ │ └── PermissionsServices.scala │ │ ├── persistence/ │ │ │ ├── Exceptions.scala │ │ │ ├── PersistenceServices.scala │ │ │ ├── conversions/ │ │ │ │ ├── AppConversions.scala │ │ │ │ ├── CardConversions.scala │ │ │ │ ├── CollectionConversions.scala │ │ │ │ ├── Conversions.scala │ │ │ │ ├── DockAppConversions.scala │ │ │ │ ├── MomentConversions.scala │ │ │ │ ├── UserConversions.scala │ │ │ │ └── WidgetConversions.scala │ │ │ └── impl/ │ │ │ ├── AndroidPersistenceServicesImpl.scala │ │ │ ├── AppPersistenceServicesImpl.scala │ │ │ ├── CardPersistenceServicesImpl.scala │ │ │ ├── CollectionPersistenceServicesImpl.scala │ │ │ ├── DockAppPersistenceServicesImpl.scala │ │ │ ├── MomentPersistenceServicesImpl.scala │ │ │ ├── PersistenceDependencies.scala │ │ │ ├── PersistenceServicesImpl.scala │ │ │ ├── UserPersistenceServicesImpl.scala │ │ │ └── WidgetPersistenceServicesImpl.scala │ │ ├── shortcuts/ │ │ │ ├── Exceptions.scala │ │ │ ├── ShortCutsServices.scala │ │ │ └── impl/ │ │ │ └── ShortCutsServicesImpl.scala │ │ ├── track/ │ │ │ ├── Exceptions.scala │ │ │ ├── TrackServices.scala │ │ │ └── impl/ │ │ │ ├── ConsoleTrackServices.scala │ │ │ └── DisableTrackServices.scala │ │ ├── utils/ │ │ │ └── ResourceUtils.scala │ │ └── widgets/ │ │ ├── Exceptions.scala │ │ ├── WidgetsServices.scala │ │ ├── impl/ │ │ │ └── WidgetsServicesImpl.scala │ │ └── utils/ │ │ ├── AppWidgetManagerCompat.scala │ │ └── impl/ │ │ ├── AppWidgetManagerImplDefault.scala │ │ └── AppWidgetManagerImplLollipop.scala │ └── test/ │ └── scala/ │ └── cards/ │ └── nine/ │ └── services/ │ ├── api/ │ │ └── impl/ │ │ ├── ApiServicesImplData.scala │ │ └── ApiServicesImplSpec.scala │ ├── apps/ │ │ └── impl/ │ │ ├── AppsServicesImplData.scala │ │ └── AppsServicesImplSpec.scala │ ├── calls/ │ │ └── impl/ │ │ ├── CallsServicesImplData.scala │ │ └── CallsServicesImplSpec.scala │ ├── connectivity/ │ │ └── impl/ │ │ ├── ConnectivityServicesImplData.scala │ │ └── ConnectivityServicesImplSpec.scala │ ├── contacts/ │ │ └── impl/ │ │ ├── ContactsServicesImplData.scala │ │ └── ContactsServicesImplSpec.scala │ ├── image/ │ │ └── impl/ │ │ ├── ImageServicesImplData.scala │ │ ├── ImageServicesImplSpec.scala │ │ └── ImageServicesTasksSpec.scala │ ├── intents/ │ │ └── impl/ │ │ ├── LauncherIntentServicesImplData.scala │ │ └── LauncherIntentServicesImplSpec.scala │ ├── persistence/ │ │ ├── data/ │ │ │ ├── AppPersistenceServicesData.scala │ │ │ ├── CardPersistenceServicesData.scala │ │ │ ├── CollectionPersistenceServicesData.scala │ │ │ ├── DockAppPersistenceServicesData.scala │ │ │ ├── MomentPersistenceServicesData.scala │ │ │ ├── UserPersistenceServicesData.scala │ │ │ └── WidgetPersistenceServicesData.scala │ │ └── impl/ │ │ ├── AppPersistenceServicesImplSpec.scala │ │ ├── CardPersistenceServicesImplSpec.scala │ │ ├── CollectionPersistenceServicesImplSpec.scala │ │ ├── DockAppPersistenceServicesImplSpec.scala │ │ ├── MomentPersistenceServicesImplSpec.scala │ │ ├── RepositoryServicesScope.scala │ │ ├── UserPersistenceServicesImplSpec.scala │ │ └── WidgetPersistenceServicesImplSpec.scala │ ├── shortcuts/ │ │ └── impl/ │ │ ├── ShortCutsServicesImplData.scala │ │ └── ShortCutsServicesImplSpec.scala │ ├── utils/ │ │ ├── ResourceUtilsData.scala │ │ └── ResourceUtilsSpec.scala │ └── widgets/ │ └── impl/ │ ├── WidgetsServicesImplData.scala │ ├── WidgetsServicesImplSpec.scala │ └── utils/ │ └── impl/ │ ├── AppWidgetManagerData.scala │ └── AppWidgetManagerImplDefaultSpec.scala ├── ninecards.debug.keystore ├── ninecards.properties.default ├── project/ │ ├── AppBuild.scala │ ├── Crashlytics.scala │ ├── Libraries.scala │ ├── Proguard.scala │ ├── ReplacePropertiesGenerator.scala │ ├── S3.scala │ ├── Settings.scala │ ├── Versions.scala │ ├── build.properties │ ├── plugins.sbt │ └── proguard.sbt ├── scripts/ │ ├── decrypt-keys.sh │ └── publishMicrosite.sh └── travis-deploy-key.enc ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ logs target tmp .history dist /out /RUNNING_PID /.ivy* # Keys /debug.properties /ninecards.properties /debug.keystore /local.properties /modules/app/local.properties # Crashlytics /modules/app/crashlytics/fabric.properties /modules/app/crashlytics/CrashlyticsManifest.xml /modules/app/src/main/assets/crashlytics-build.properties /modules/app/src/main/res/values/com_crashlytics_export_strings.xml # sbt specific /.sbt .cache/ .history/ .lib/ dist/* target/ lib_managed/ src_managed/ project/boot/ project/plugins/project/ project/project project/target /.activator fabric.properties # Scala-IDE specific .scala_dependencies .worksheet #Eclipse specific .classpath .project .cache .settings/ #IntelliJ IDEA specific .idea/ /.idea_modules /.idea /*.iml #Proguard proguard-sbt.txt #Gen modules/api/gen/ modules/repository/gen/ #Keys travis-deploy-key travis-deploy-key.pub ================================================ FILE: .scalafmt.conf ================================================ style = defaultWithAlign maxColumn = 100 continuationIndent.callSite = 2 newlines { sometimesBeforeColonInMethodReturnType = false } align { arrowEnumeratorGenerator = false ifWhileOpenParen = false openParenCallSite = false openParenDefnSite = false } docstrings = JavaDoc rewrite { rules = [SortImports, RedundantBraces] redundantBraces.maxLines = 1 } ================================================ FILE: .travis.yml ================================================ sudo: false language: scala jdk: - oraclejdk8 scala: - 2.11.7 addons: apt: packages: - libc6-i386 - lib32z1 - lib32stdc++6 before_install: - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then bash scripts/decrypt-keys.sh; fi - export PATH=${PATH}:./vendor/bundle - wget http://dl.google.com/android/android-sdk_r24-linux.tgz - tar xf android-sdk_r24-linux.tgz - export ANDROID_HOME=$PWD/android-sdk-linux - export ANDROID_SDK_HOME=$PWD/android-sdk-linux - export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools - echo yes | android update sdk --all --filter platform-tools --no-ui - echo yes | android update sdk --all --filter build-tools-25.0.0 --no-ui - echo yes | android update sdk --all --filter android-24 --no-ui - echo yes | android update sdk --all --filter extra-android-support --no-ui - echo yes | android update sdk --all --filter extra-android-m2repository --no-ui - echo yes | android update sdk --all --filter extra-google-m2repository --no-ui install: - rvm use 2.2.3 --install --fuzzy - gem update --system - gem install sass - gem install jekyll -v 3.2.1 script: - sbt ++$TRAVIS_SCALA_VERSION "project commons" coverage test - sbt ++$TRAVIS_SCALA_VERSION "project api" coverage test - sbt ++$TRAVIS_SCALA_VERSION "project repository" coverage test - sbt ++$TRAVIS_SCALA_VERSION "project services" coverage test - sbt ++$TRAVIS_SCALA_VERSION "project process" coverage test - sbt ++$TRAVIS_SCALA_VERSION "project app" coverage test after_success: - sbt ++$TRAVIS_SCALA_VERSION "project tests" test:coverageAggregate - bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN} - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then bash scripts/publishMicrosite.sh; fi ================================================ FILE: CHANGELOG.md ================================================ ## 2017/02/06 - Version Name: 2.0.11-rc2 - Version Code: 68 * FlowUp have to be activated from Developer Options ## 2017/01/30 - Version Name: 2.0.10-rc1 - Version Code: 67 * Improved the experience when the user back to the launcher from App Drawer * New technical documentation * Changed URL for sharing collections * Some bugs fixed in UI ## 2016/12/23 - Version Name: 2.0.9-beta - Version Code: 66 * The user can add Bluetooth devices to Moment * Improved Widgets screen * Added Apptentive * The user can disable Google Analytics * Added tests for jobs * Some bugs fixed in UI ## 2016/12/16 - Version Name: 2.0.8-beta - Version Code: 65 * Drag&Drop for managing widgets * Share from other apps have been improved * Bugs fixed in shortcuts * Bugs fixed in subscriptions * Added tests for jobs * Some bugs fixed in UI ## 2016/12/02 - Version Name: 2.0.7-beta - Version Code: 64 * New About screen with Scala libraries and Team * Number of views in Public Collections is updated when the user adds it to his collections * New actions to access to Google Play and Phone in AppDrawer * Fixed some problems with AppDrawer in empty lists * Fixed problems in Wizard when some steps launch an exception * Added tests for jobs * Some bugs fixed in UI ## 2016/11/28 - Version Name: 2.0.6-alpha - Version Code: 63 * Removed clock in Moment bar * Added options to menu: wallpaper, settings and widgets * Upgrade libraries: SBT-Android plugin, Cats and Monix * Added tests for jobs * Some bugs fixed in UI ## 2016/11/21 - Version Name: 2.0.5-alpha - Version Code: 62 * We have removed the collapse toolbar in Collections Screen * Wallpaper is moved when the user swipe right/left in workspaces. You can be disabled this behavior in preferences * Bug fixed related to the app opens Wizard recursively * English texts reviewed * New message in Car, Music and Out & About Moment for explaining the special conditions of this moments * All events tracked in Google Analytics * Added tests for jobs ## 2016/11/16 - Version Name: 2.0.4-alpha - Version Code: 61 * New Wizards Inline. 9Cards Team shows you how you can use the launcher * The user can filter apps and contacts in the dialogs when he wants to add it to the collection * FlowUp integrated. Thanks to Pedro of Karumi * Now, the dialogs are using BottomSheetFragment in order to the app has the appearance like Nougat * New header in Moment Dialog * New events tracked in Google Analytics * Libraries upgraded: Android API to 24, Android Support. Google Services and Macroid 2.0 * Added tests for jobs * Some bugs fixed in UI ## 2016/11/09 - Version Name: 2.0.3-alpha - Version Code: 60 * Changes in the Wizard: new design for loading previous configuration and some changes in new configuration * Now you can select several multiple apps in AppsDialog for adding to collection * Bugs fixed in Dark Theme * We have created the first version of microsite * First revision of all text in the app * New Developer Options (remember you have to do long-click in Settings button for activating Dev Ops) * New events tracked in Google Analytics * Added test for jobs in Launcher screen * Some bugs fixed in UI ## 2016/10/31 - Version Name: 2.0.2-alpha - Version Code: 59 * New design of App Drawer. Now the tabs are Applications and Contacts and you can filter from the new option menu * We have fixed the problems with the widgets. The widgets are updated when the user changes the size and loaded fine the first time * Bugs fixed on right drawer where appears the apps list of the current collection * New "Add Card" action in the options of the toolbar on the collections screen * Improved the exit animation on collection screen * Bugs fixed in the top bar on the launcher and others minor bugs fixed too ## 2016/10/25 - Version Name: 2.0.1-alpha - Version Code: 58 * New categorization: bugs fixed, new API models and improves the order * The moments change automatically when the wifi is changed, plug in headphone or you drive your car * New Moments Out and About and Sport * You can add, edit or remove your moments * You can pin and unpin your moment from the top bar * You can drag applications from dock in order to reorder apps in the dock or remove * New preferences for show/hide icons in the moment top bar * New subscription screen design for fixing problem with Android 4.4 or lower * Added two new developer options to: * Change backend URL at runtime. * Enable/disable Stetho. * Refactorized all persistence models. * Refactorized all api services models. ## 2016/10/14 - Version Name: 2.0-alpha - Version Code: 57 * First alpha version ================================================ FILE: CONTRIBUTING.md ================================================ ## How Can I Contribute? The issues have been integrated into all stages of the development process. This way, the work is coordinated through the so-called Agile Management following Scrum techniques. For this process, we used Projects in GitHub. First, you should create a new issue with the bug or new behaviour that you want to implement in 9 Cards. You also can contribute implementing the [existing issues in 9 Cards](https://github.com/47deg/nine-cards-v2/issues). When you create a new issue you have to add [the labels](https://github.com/47deg/nine-cards-v2/labels) so other developers can understand the problem or new behaviour. The mandatory labels are: - **Story Points:** Rate the relative work effort in a Fibonacci-like format: 1, 2, 3, 5, or 8. If we are referring to time, the correspondence for every SP is: 2 hours, 1 day, 2 or 3 days, 1 week and 2 weeks. If you want to put 8 SP on one issue, you should consider dividing the issue. - **Server or Client:** You should add a new label if the issue is for the [server](https://github.com/47deg/nine-cards-backend) or [client](https://github.com/47deg/nine-cards-v2). In addition, if it's a client issue, you can add a `ui` label if you're only working on UI. - **Expertise Level:** Add the label for `beginner`, `intermediate` or `advanced`. You have more labels that you can use if you think they're relevent for other developers such as `bug`, `critical`, `test`, and so on. When you have selected the issue that you want to work on, you must add the issue in [the board](https://github.com/47deg/nine-cards-v2/projects) (Server or Client) in the `In progess` column. After that, you should create a new `branch` where you'll implement the code. The name of the branch is important: - [Github Name]-[Issue Number]-[Small Description] For example, `47dev-1213-Fixing_Tests` Every issue passes through four statuses: - **Development:** you are resolving the issue. The issue is in `In Progress` column. - **Code review:** another person is reviewing the style of the code. You can assign the issue to another developer. The issue is in the `Code review` column. You need a `LGTM!` or `Thumbs up` to pass on to the next step. - **QA:** another person verifies that the code resolves the issue. The issue is in `QA` column. If the branch covers the description as expected, you can pass on to the next step. - **Ready to Master:** The issue is in the `Ready to Master` column. You have to wait until we include the code in master. If you finish the process, you'll be a contributor of 9 Cards and we'll be happy to have you! ================================================ FILE: ISSUE_TEMPLATE.md ================================================ ### Description [Description of the issue] ### Steps to Reproduce 1. [First Step] 2. [Second Step] 3. [and so on...] **Expected behavior:** [What you expect to happen] **Actual behavior:** [What actually happens] ================================================ FILE: LICENSE ================================================ Copyright 2016 47 Degrees, LLC. 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. Regarding the use of a piece of code from the cards.nine.utils.SystemBarTintManager.java by readyState Software: Copyright (C) 2013 readyState Software Ltd And cards.nine.app.ui.components.layouts.SlidingTabLayout.scala by The Android Open Source Project Copyright (C) 2013 The Android Open Source Project 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. 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. ================================================ FILE: PULL_REQUEST_TEMPLATE.md ================================================ # Pull Request Checklist * [ ] Have you read through the [contributor guidelines](https://github.com/47deg/nine-cards-v2/blob/master/CONTRIBUTING.md)? * [ ] Have you added copyright headers to new files? * [ ] Have you updated the documentation? * [ ] Have you added tests for any changed functionality? ## Fixes Fixes #xxxx ## Purpose What does this PR do? ================================================ FILE: README.md ================================================ [![Join the conversation on Gitter](https://img.shields.io/gitter/room/47deg/nine-cards-v2.svg)](https://gitter.im/47deg/nine-cards-v2) # 9-Cards 9 Cards was an open source home launcher for Android, whose code was all written in Scala. It provided features for organizing apps into collections, giving quick and easy access to the apps more frequently used, at will. Since January 2019, the 9-Cards project is now abandoned, and the App is no longer available in the Google Play store. The source code repo has been left here, for those wishing to study or to re-create it, published under the Apache License. For a full list of changes please view:[Changelog](CHANGELOG.md). For instructions for building an using the app, see the [old README](README_old.md). [![Google Play](https://cloud.githubusercontent.com/assets/456025/22688567/25acb99e-ed2d-11e6-87df-cda19849fa84.png)](https://play.google.com/store/apps/details?id=com.fortysevendeg.ninecardslauncher) ================================================ FILE: README_old.md ================================================ [![Build Status](https://travis-ci.com/47deg/nine-cards-v2.svg?token=wpV9KDdSpewCkwVHdFCg&branch=master)](https://travis-ci.com/47deg/nine-cards-v2) [![Join the conversation on Gitter](https://img.shields.io/gitter/room/47deg/nine-cards-v2.svg)](https://gitter.im/47deg/nine-cards-v2) # 9 Cards v2 [![Google Play](https://cloud.githubusercontent.com/assets/456025/22688567/25acb99e-ed2d-11e6-87df-cda19849fa84.png)](https://play.google.com/store/apps/details?id=com.fortysevendeg.ninecardslauncher) 9 Cards is an open source home launcher for Android written in Scala. This mobile application does the bulk of the work for you, organizing your apps into collections, giving you quick and easy access to the apps you need most, when you need them. For a full list of changes please view:[Changelog](CHANGELOG.md) # Table of Contents 1. [Prerequisites](#prerequisites) 2. [Compile and Run](#compile-and-run) 3. [Properties File](#properties-file) 4. [Troubleshooting](#troubleshooting) ## Prerequisites ### SBT * [Download](http://www.scala-sbt.org/download.html) and install sbt. ### Android SDK * [Download](https://developer.android.com/studio/index.html#downloads). You only need the command line tools. * Set `ANDROID_HOME` environment variable pointing to the root folder. ### Android Device You need an Android device and must [enable USB debugging](https://www.google.es/search?q=android+activate+developer+mode&oq=android+active+developer). ### Google Project 9 Cards needs the following Google APIs: * Google Drive API for storing your devices in the cloud. * Google Plus API for authenticating the user requests. You need to create a project in the Google API Console with these two APIs enabled. For that, you have 2 choices: * Normal Mode (Recommended): You must create the keys in the Google Developers Console. You only need 10 minutes for that. * Easy Mode: We give you the keys and you don't have to create the project in the Google Developers Console. ### Normal Mode: Google Project 1. Go to the [Google Developers Console](https://console.developers.google.com/apis/library?project=_). 2. From the project drop-down, select a [project](https://support.google.com/cloud/answer/6158853), or create a new one. #### Google Drive API 1. Enable the Google Drive API service: 1. In the sidebar under "API Manager", select *Library*. 2. In the list of Google APIs, search for the Google Drive API service. 3. Select Google Drive API from the results list. 4. Press the Enable API button. 2. In the sidebar under "API Manager", select Credentials. 3. In the Credentials tab, select the *Create credentials* drop-down list, and choose OAuth client ID. 4. Select *Android* as *Application type*. 5. Enter a key Name. 6. [Find your certificate SHA1 fingerprint](https://developers.google.com/android/guides/client-auth) and paste it in the form where requested. 7. Enter `com.fortysevendeg.ninecardslauncher` in the package name field. 8. Click on "Create". [More info](https://developers.google.com/drive/android/auth) #### Google Plus API 1. Enable the Google Plus API service: 1. In the sidebar under "API Manager", select *Library*. 2. In the list of Google APIs, search for the Google+ API service. 3. Select Google+ API from the results list. 4. Press the Enable API button. 2. In the sidebar under "API Manager", select *Credentials*. 3. In the Credentials tab, select the *Create credentials* drop-down list, and choose OAuth client ID. 4. Select *Web application* as *Application type*. 5. Enter a key Name then select Create. 6. Then copy the *client ID* of the newly generated credential. ### Easy Mode: Google Project You must add the following content to `ninecards.properties` file: ``` # Backend V2 backend.v2.url=https://nine-cards.herokuapp.com backend.v2.clientid=411191100294-sjhinp1i2gkp46u36ii7m16v9hog64nn.apps.googleusercontent.com ``` and you must launch SBT with the following command: ``` $ sbt -mem 2048 -Ddebug ``` At the end of the compilation, previously for installing on a cellphone, you must put the password of the keystore: ``` Enter keystore password: ``` The password is `android`. Note: If you plan on working on this project, please consider using the `Default Mode` ## Compile and Run To compile the project: * Clone this GitHub project to your computer: ``` $ git clone git@github.com:47deg/nine-cards-v2.git ``` * Add a `ninecards.properties` file (See [Add Debug Keys](#properties-file) section). * You need to set the heap size to at least 2M: ``` $ sbt -mem 2048 ``` * Verify that your device is attached: ``` > devices ``` The output should look like: ``` [info] Serial Model Battery % Android Version [info] ---------------------- ---------------- --------- --------------- [info] XXXXXXXXXX Nexus 6 66% 6.0.1 API 23 ``` * Now you're ready to run the project, just execute: ``` > run ``` ## Properties File You need to add a `ninecards.properties` file in the project root folder. This file provides some keys for different third party services. We'll see all these down below. To begin with, you can use the template provided in the root folder: ``` $ cp ninecards.properties.default ninecards.properties ``` This is the content: ``` # Backend V2 backend.v2.url= backend.v2.clientid= # Third Parties crashlytics.enabled=false crashlytics.apikey= crashlytics.apisecret= strictmode.enabled=false analytics.enabled=false analytics.trackid= # Firebase firebase.enabled=false firebase.url= firebase.google.appid= firebase.google.apikey= firebase.gcm.senderid= firebase.clientid= # FlowUp flowup.enabled=false flowup.apikey= ``` ### Backend V2 (Mandatory) * `backend.v2.url`: Defines the URL for the Backend. Visit the [GitHub project](https://github.com/47deg/nine-cards-backend) for more information. * `backend.v2.clientid`: This value is used for requesting a token id that will be used by the Backend to authenticate the user. It's the *client id* obtained in the [Google Plus API section](#google-plus-api). ### Third Parties (Optional) **[Crashlytics](https://try.crashlytics.com/)** * `crashlytics.enabled`: Enables or disables the Crashlytics service. * `crashlytics.apikey` & `crashlytics.apisecret`: These values are fetched from your [Crashlytics organization page](https://www.fabric.io/settings/organizations). **[Strict Mode](https://developer.android.com/reference/android/os/StrictMode.html)** * `strictmode.enabled`: Enables or disables the Strict Mode. **[Google Analytics](https://developers.google.com/analytics/)** * `analytics.enabled`: Enables or disables the Google Analytics service. * `analytics.trackid`: You can use your own tracking ID. See how to [find your tracking code, tracking ID, and property number](https://support.google.com/analytics/answer/1032385). **[FlowUp](http://flowup.io)** * `flowup.enabled`: Enables or disables the FlowUp service. * `flowup.apikey`: These values are fetched from your [FlowUp account](http://flowup.io). ### Google Firebase (Optional) Google Firebase is used for push notifications. **[Google Firebase](https://firebase.google.com/)** 1. Create a Firebase project in the [Firebase console](https://firebase.google.com/console/), if you don't already have one. If you already have an existing Google project associated with your mobile app, click Import Google Project. Otherwise, click Create New Project. 2. Add a new app in *Project Settings* -> *General* 3. Select the newly created app and download the `google-services.json` 4. Open the file in a text editor. All bellow properties are taken from this file: * `firebase.enabled`: Enables or disables the Google Firebase service * `firebase.url`: Property `project_info.firebase_url` * `firebase.google.appid`: Property `client[0].client_info.mobilesdk_app_id` * `firebase.google.apikey`: Property `client[0].api_key[0].current_key` * `firebase.gcm.senderid`: Property `project_info.project_number` * `firebase.clientid`: Property `client[0].oauth_client[x].client_id` where x is the index of one element with `client_type` == 3 ## Troubleshooting This section contains information about possible problems that may occur compiling 9 Cards. ### Ubuntu: ProcessException When you compile the project, it's possible that you will have this error: `com.android.ide.common.process.ProcessException` It's a problem in the 64-bit system and you need to install the `ia32-libs`. You should install the following next: `sudo apt-get install lib32stdc++6 lib32z1` More information can be found [here](http://stackoverflow.com/questions/22701405/aapt-ioexception-error-2-no-such-file-or-directory-why-cant-i-build-my-grad). ### Ubuntu: Launching IntelliJ from unity panel If you are using IntelliJ from unity panel it's possible that the app isn't finding the `ANDROID_HOME` environment variable. Unity launcher doesn't source the user's environment from `.bashrc` and you should include the `ANDROID_HOME` in `/etc/environment` and IntelliJ will work fine. ================================================ FILE: codecov.yml ================================================ codecov: bot: 47degdev comment: layout: header, changes, diff coverage: ignore: - modules/commons-tests/* - modules/mock-android/* status: patch: false ================================================ FILE: modules/api/build.sbt ================================================ platformTarget in Android := "android-24" ================================================ FILE: modules/api/src/main/AndroidManifest.xml ================================================ ================================================ FILE: modules/api/src/main/res/values/strings.xml ================================================ Multi-module Sample ================================================ FILE: modules/api/src/main/scala/cards/nine/api/rest/client/Exceptions.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client import cards.nine.commons.services.TaskService.NineCardException case class ServiceClientException(message: String, cause: Option[Throwable] = None) extends RuntimeException(message) with NineCardException { cause map initCause } trait ImplicitsServiceClientExceptions { implicit def accountsServicesExceptionConverter = (t: Throwable) => ServiceClientException(t.getMessage, Option(t)) } ================================================ FILE: modules/api/src/main/scala/cards/nine/api/rest/client/ServiceClient.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client import cards.nine.commons.NineCardExtensions._ import cards.nine.commons.services.TaskService import cards.nine.commons.services.TaskService._ import cards.nine.api.rest.client.http.{HttpClient, HttpClientResponse} import cards.nine.api.rest.client.messages.ServiceClientResponse import monix.eval.Task import play.api.libs.json.{Json, Reads, Writes} import scala.util.{Failure, Success, Try} class ServiceClient(httpClient: HttpClient, val baseUrl: String) extends ImplicitsServiceClientExceptions { def get[Res]( path: String, headers: Seq[(String, String)] = Seq.empty, reads: Option[Reads[Res]] = None, emptyResponse: Boolean = false ): TaskService[ServiceClientResponse[Res]] = for { clientResponse <- httpClient .doGet(baseUrl.concat(path), headers) .resolve[ServiceClientException] response <- readResponse(clientResponse, reads, emptyResponse) } yield ServiceClientResponse(clientResponse.statusCode.intValue, response) def emptyPost[Res]( path: String, headers: Seq[(String, String)] = Seq.empty, reads: Option[Reads[Res]] = None, emptyResponse: Boolean = false ): TaskService[ServiceClientResponse[Res]] = for { clientResponse <- httpClient .doPost(baseUrl.concat(path), headers) .resolve[ServiceClientException] response <- readResponse(clientResponse, reads, emptyResponse) } yield ServiceClientResponse(clientResponse.statusCode, response) def post[Req, Res]( path: String, headers: Seq[(String, String)] = Seq.empty, body: Req, reads: Option[Reads[Res]] = None, emptyResponse: Boolean = false )(implicit writes: Writes[Req]): TaskService[ServiceClientResponse[Res]] = for { clientResponse <- httpClient .doPost[Req](baseUrl.concat(path), headers, body) .resolve[ServiceClientException] response <- readResponse(clientResponse, reads, emptyResponse) } yield ServiceClientResponse(clientResponse.statusCode, response) def emptyPut[Res]( path: String, headers: Seq[(String, String)] = Seq.empty, reads: Option[Reads[Res]] = None, emptyResponse: Boolean = false ): TaskService[ServiceClientResponse[Res]] = for { clientResponse <- httpClient .doPut(baseUrl.concat(path), headers) .resolve[ServiceClientException] response <- readResponse(clientResponse, reads, emptyResponse) } yield ServiceClientResponse(clientResponse.statusCode, response) def put[Req, Res]( path: String, headers: Seq[(String, String)] = Seq.empty, body: Req, reads: Option[Reads[Res]] = None, emptyResponse: Boolean = false )(implicit writes: Writes[Req]): TaskService[ServiceClientResponse[Res]] = for { httpResponse <- httpClient .doPut[Req](baseUrl.concat(path), headers, body) .resolve[ServiceClientException] response <- readResponse(httpResponse, reads, emptyResponse) } yield ServiceClientResponse(httpResponse.statusCode, response) def delete[Res]( path: String, headers: Seq[(String, String)] = Seq.empty, reads: Option[Reads[Res]] = None, emptyResponse: Boolean = false ): TaskService[ServiceClientResponse[Res]] = for { clientResponse <- httpClient .doDelete(baseUrl.concat(path), headers) .resolve[ServiceClientException] response <- readResponse(clientResponse, reads, emptyResponse) } yield ServiceClientResponse(clientResponse.statusCode, response) private def readResponse[T]( clientResponse: HttpClientResponse, maybeReads: Option[Reads[T]], emptyResponse: Boolean ): TaskService[Option[T]] = { def isError: Boolean = clientResponse.statusCode >= 400 && clientResponse.statusCode < 600 TaskService { Task { if (isError) { Left( ServiceClientException( s"Error making request. Status code ${clientResponse.statusCode}")) } else { (clientResponse.body, emptyResponse, maybeReads) match { case (Some(d), false, Some(r)) => transformResponse[T](d, r) case (None, false, _) => Left(ServiceClientException("No content")) case (Some(d), false, None) => Left(ServiceClientException("No transformer found for type")) case _ => Right(None) } } } } } private def transformResponse[T]( content: String, reads: Reads[T] ): Either[ServiceClientException, Some[T]] = Try(Json.parse(content).as[T](reads)) match { case Success(s) => Right(Some(s)) case Failure(e) => Left(ServiceClientException(message = e.getMessage, cause = Some(e))) } } ================================================ FILE: modules/api/src/main/scala/cards/nine/api/rest/client/http/Exceptions.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client.http import cards.nine.commons.services.TaskService.NineCardException case class HttpClientException(message: String, cause: Option[Throwable] = None) extends RuntimeException(message) with NineCardException { cause map initCause } trait ImplicitsHttpClientExceptions { implicit def httpClientExceptionConverter = (t: Throwable) => HttpClientException(t.getMessage, Option(t)) } ================================================ FILE: modules/api/src/main/scala/cards/nine/api/rest/client/http/HttpClient.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client.http import cards.nine.commons.services.TaskService.TaskService import play.api.libs.json.Writes trait HttpClient { def doGet( url: String, httpHeaders: Seq[(String, String)] ): TaskService[HttpClientResponse] def doDelete( url: String, httpHeaders: Seq[(String, String)] ): TaskService[HttpClientResponse] def doPost( url: String, httpHeaders: Seq[(String, String)] ): TaskService[HttpClientResponse] def doPost[Req: Writes]( url: String, httpHeaders: Seq[(String, String)], body: Req ): TaskService[HttpClientResponse] def doPut( url: String, httpHeaders: Seq[(String, String)] ): TaskService[HttpClientResponse] def doPut[Req: Writes]( url: String, httpHeaders: Seq[(String, String)], body: Req ): TaskService[HttpClientResponse] } ================================================ FILE: modules/api/src/main/scala/cards/nine/api/rest/client/http/HttpClientResponse.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client.http case class HttpClientResponse(statusCode: Int, body: Option[String]) ================================================ FILE: modules/api/src/main/scala/cards/nine/api/rest/client/http/OkHttpClient.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client.http import cards.nine.commons.CatchAll import cards.nine.commons.services.TaskService import cards.nine.commons.services.TaskService._ import cards.nine.api.rest.client.http.Methods._ import play.api.libs.json.{Json, Writes} import scala.collection.JavaConverters._ class OkHttpClient(okHttpClient: okhttp3.OkHttpClient = new okhttp3.OkHttpClient) extends HttpClient with ImplicitsHttpClientExceptions { val jsonMediaType = okhttp3.MediaType.parse("application/json") val textPlainMediaType = okhttp3.MediaType.parse("text/plain") override def doGet( url: String, httpHeaders: Seq[(String, String)] ): TaskService[HttpClientResponse] = doMethod(GET, url, httpHeaders) override def doDelete( url: String, httpHeaders: Seq[(String, String)] ): TaskService[HttpClientResponse] = doMethod(DELETE, url, httpHeaders) override def doPost( url: String, httpHeaders: Seq[(String, String)] ): TaskService[HttpClientResponse] = doMethod(POST, url, httpHeaders) override def doPost[Req: Writes]( url: String, httpHeaders: Seq[(String, String)], body: Req ): TaskService[HttpClientResponse] = doMethod(POST, url, httpHeaders, Some(Json.toJson(body).toString())) override def doPut( url: String, httpHeaders: Seq[(String, String)] ): TaskService[HttpClientResponse] = doMethod(PUT, url, httpHeaders) override def doPut[Req: Writes]( url: String, httpHeaders: Seq[(String, String)], body: Req ): TaskService[HttpClientResponse] = doMethod(PUT, url, httpHeaders, Some(Json.toJson(body).toString())) private[this] def doMethod[T]( method: Method, url: String, httpHeaders: Seq[(String, String)], body: Option[String] = None, responseHandler: okhttp3.Response => T = defaultResponseHandler _): TaskService[T] = TaskService { CatchAll[HttpClientException] { val builder = createBuilderRequest(url, httpHeaders) val request = (method match { case GET => builder.get() case DELETE => builder.delete() case POST => builder.post(createBody(body)) case PUT => builder.put(createBody(body)) }).build() responseHandler(okHttpClient.newCall(request).execute()) } } private[this] def defaultResponseHandler(response: okhttp3.Response): HttpClientResponse = HttpClientResponse(response.code(), Option(response.body()) map (_.string())) private[this] def createBuilderRequest( url: String, httpHeaders: Seq[(String, String)]): okhttp3.Request.Builder = new okhttp3.Request.Builder().url(url).headers(createHeaders(httpHeaders)) private[this] def createHeaders(httpHeaders: Seq[(String, String)]): okhttp3.Headers = okhttp3.Headers.of(httpHeaders.map { case (key, value) => key -> value }.toMap.asJava) private[this] def createBody(body: Option[String]) = body match { case Some(b) => okhttp3.RequestBody.create(jsonMediaType, b) case _ => okhttp3.RequestBody.create(textPlainMediaType, "") } } object Methods { sealed trait Method case object GET extends Method case object DELETE extends Method case object POST extends Method case object PUT extends Method } ================================================ FILE: modules/api/src/main/scala/cards/nine/api/rest/client/messages/Messages.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client.messages case class ServiceClientResponse[T](statusCode: Int, data: Option[T]) class ServiceClientException(message: String) extends RuntimeException(message) ================================================ FILE: modules/api/src/main/scala/cards/nine/api/version1/ApiService.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version1 import cards.nine.commons.services.TaskService.TaskService import cards.nine.api.rest.client.ServiceClient import cards.nine.api.rest.client.messages.ServiceClientResponse import play.api.libs.json.{Reads, Writes} class ApiService(serviceClient: ServiceClient) { val prefixPathUser = "/users" val prefixPathUserConfig = "/ninecards/userconfig" def baseUrl: String = serviceClient.baseUrl def login(user: User, headers: Seq[(String, String)])( implicit reads: Reads[User], writes: Writes[User]): TaskService[ServiceClientResponse[User]] = serviceClient .post[User, User](path = prefixPathUser, headers = headers, body = user, reads = Some(reads)) def getUserConfig( headers: Seq[(String, String)] )(implicit reads: Reads[UserConfig]): TaskService[ServiceClientResponse[UserConfig]] = serviceClient .get[UserConfig](path = prefixPathUserConfig, headers = headers, reads = Some(reads)) } ================================================ FILE: modules/api/src/main/scala/cards/nine/api/version1/JsonImplicits.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version1 object JsonImplicits { import play.api.libs.json._ implicit val authAnonymousReads = Json.reads[AuthAnonymous] implicit val authFacebookReads = Json.reads[AuthFacebook] implicit val authTwitterReads = Json.reads[AuthTwitter] implicit val authGoogleDeviceReads = Json.reads[AuthGoogleDevice] implicit val authGoogleReads = Json.reads[AuthGoogle] implicit val authDataReads = Json.reads[AuthData] implicit val userReads = Json.reads[User] implicit val userConfigTimeSlotReads = Json.reads[UserConfigTimeSlot] implicit val userConfigUserLocationReads = Json.reads[UserConfigUserLocation] implicit val userConfigCollectionItemReads = Json.reads[UserConfigCollectionItem] implicit val userConfigCollectionReads = Json.reads[UserConfigCollection] implicit val userConfigProfileImageReads = Json.reads[UserConfigProfileImage] implicit val userConfigStatusInfoReads = Json.reads[UserConfigStatusInfo] implicit val userConfigGeoInfoReads = Json.reads[UserConfigGeoInfo] implicit val userConfigDeviceReads = Json.reads[UserConfigDevice] implicit val userConfigPlusProfileReads = Json.reads[UserConfigPlusProfile] implicit val userConfigReads = Json.reads[UserConfig] implicit val authAnonymousWrites = Json.writes[AuthAnonymous] implicit val authFacebookWrites = Json.writes[AuthFacebook] implicit val authTwitterWrites = Json.writes[AuthTwitter] implicit val authGoogleDeviceWrites = Json.writes[AuthGoogleDevice] implicit val authGoogleWrites = Json.writes[AuthGoogle] implicit val authDataWrites = Json.writes[AuthData] implicit val userWrites = Json.writes[User] implicit val userConfigTimeSlotWrites = Json.writes[UserConfigTimeSlot] implicit val userConfigUserLocationWrites = Json.writes[UserConfigUserLocation] implicit val userConfigCollectionItemWrites = Json.writes[UserConfigCollectionItem] implicit val userConfigCollectionWrites = Json.writes[UserConfigCollection] implicit val userConfigProfileImageWrites = Json.writes[UserConfigProfileImage] implicit val userConfigStatusInfoWrites = Json.writes[UserConfigStatusInfo] implicit val userConfigGeoInfoWrites = Json.writes[UserConfigGeoInfo] implicit val userConfigDeviceWrites = Json.writes[UserConfigDevice] implicit val userConfigPlusProfileWrites = Json.writes[UserConfigPlusProfile] implicit val userConfigWrites = Json.writes[UserConfig] } ================================================ FILE: modules/api/src/main/scala/cards/nine/api/version1/Model.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version1 import play.api.libs.json._ case class User( _id: Option[String], sessionToken: Option[String], username: Option[String], password: Option[String], email: Option[String], authData: Option[AuthData]) case class AuthData( google: Option[AuthGoogle], facebook: Option[AuthFacebook], twitter: Option[AuthTwitter], anonymous: Option[AuthAnonymous]) case class AuthGoogleDevice( name: String, deviceId: String, secretToken: String, permissions: Seq[String]) case class AuthGoogle(email: String, devices: Seq[AuthGoogleDevice]) case class AuthTwitter( id: String, screenName: String, consumerKey: String, consumerSecret: String, authToken: String, authTokenSecret: String, key: String, secretKey: String) case class AuthFacebook(id: String, accessToken: String, expirationDate: Long) case class AuthAnonymous(id: String) case class UserConfig( _id: String, email: String, plusProfile: UserConfigPlusProfile, devices: Seq[UserConfigDevice], geoInfo: UserConfigGeoInfo, status: UserConfigStatusInfo) case class UserConfigPlusProfile(displayName: String, profileImage: UserConfigProfileImage) case class UserConfigDevice( deviceId: String, deviceName: String, collections: Seq[UserConfigCollection]) case class UserConfigGeoInfo( homeMorning: Option[UserConfigUserLocation], homeNight: Option[UserConfigUserLocation], work: Option[UserConfigUserLocation], current: Option[UserConfigUserLocation]) case class UserConfigStatusInfo( products: Seq[String], friendsReferred: Int, themesShared: Int, collectionsShared: Int, customCollections: Int, earlyAdopter: Boolean, communityMember: Boolean, joinedThrough: Option[String], tester: Boolean) case class UserConfigProfileImage(imageType: Int, imageUrl: String, secureUrl: Option[String]) case class UserConfigCollection( name: String, originalSharedCollectionId: Option[String], sharedCollectionId: Option[String], sharedCollectionSubscribed: Option[Boolean], items: Seq[UserConfigCollectionItem], collectionType: String, constrains: Seq[String], wifi: Seq[String], occurrence: Seq[String], icon: String, radius: Int, lat: Double, lng: Double, alt: Double, category: Option[String]) case class UserConfigCollectionItem( itemType: String, title: String, metadata: JsValue, categories: Option[Seq[String]]) case class UserConfigUserLocation( wifi: String, lat: Double, lng: Double, occurrence: Seq[UserConfigTimeSlot]) case class UserConfigTimeSlot(from: String, to: String, days: Seq[Int]) ================================================ FILE: modules/api/src/main/scala/cards/nine/api/version2/ApiService.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version2 import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import cards.nine.commons.services.TaskService.TaskService import cards.nine.api.rest.client.http.HttpClientException import cards.nine.api.rest.client.messages.ServiceClientResponse import cards.nine.api.rest.client.{ServiceClient, ServiceClientException} import play.api.libs.json.{Reads, Writes} class ApiService(serviceClient: ServiceClient) { def baseUrl: String = serviceClient.baseUrl type ApiException = HttpClientException with ServiceClientException private[this] val headerContentType = "Content-Type" private[this] val headerContentTypeValue = "application/json" private[this] val headerAuthToken = "X-Auth-Token" private[this] val headerSessionToken = "X-Session-Token" private[this] val headerAndroidId = "X-Android-ID" private[this] val headerMarketLocalization = "X-Android-Market-Localization" private[this] val headerMarketLocalizationValue = "en-US" private[this] val headerAndroidMarketToken = "X-Google-Play-Token" private[this] val loginPath = "/login" private[this] val installationsPath = "/installations" private[this] val collectionsPath = "/collections" private[this] val subscriptionsPath = s"$collectionsPath/subscriptions" private[this] val latestCollectionsPath = s"$collectionsPath/latest" private[this] val topCollectionsPath = s"$collectionsPath/top" private[this] val viewsPath = s"/views" private[this] val applicationsPath = "/applications" private[this] val categorizePath = s"$applicationsPath/categorize" private[this] val rankPath = s"$applicationsPath/rank" private[this] val rankAppsByMomentPath = s"$applicationsPath/rank-by-moments" private[this] val rankWidgetsByMomentPath = "/widgets/rank" private[this] val categorizeDetailPath = s"$applicationsPath/details" private[this] val recommendationsPath = "/recommendations" private[this] val searchPath = s"$applicationsPath/search" def login(request: ApiLoginRequest)( implicit reads: Reads[ApiLoginResponse], writes: Writes[ApiLoginRequest]): TaskService[ServiceClientResponse[ApiLoginResponse]] = serviceClient.post[ApiLoginRequest, ApiLoginResponse]( path = loginPath, headers = Seq((headerContentType, headerContentTypeValue)), body = request, reads = Some(reads)) def installations(request: InstallationRequest, header: ServiceHeader)( implicit reads: Reads[InstallationResponse], writes: Writes[InstallationRequest]): TaskService[ ServiceClientResponse[InstallationResponse]] = serviceClient.put[InstallationRequest, InstallationResponse]( path = installationsPath, headers = createHeaders(installationsPath, header), body = request, reads = Some(reads)) def latestCollections(category: String, offset: Int, limit: Int, header: ServiceMarketHeader)( implicit reads: Reads[CollectionsResponse]): TaskService[ ServiceClientResponse[CollectionsResponse]] = { val path = s"$latestCollectionsPath/$category/$offset/$limit" serviceClient.get[CollectionsResponse]( path = path, headers = createHeaders(path, header), reads = Some(reads)) } def topCollections(category: String, offset: Int, limit: Int, header: ServiceMarketHeader)( implicit reads: Reads[CollectionsResponse]): TaskService[ ServiceClientResponse[CollectionsResponse]] = { val path = s"$topCollectionsPath/$category/$offset/$limit" serviceClient.get[CollectionsResponse]( path = path, headers = createHeaders(path, header), reads = Some(reads)) } def createCollection(request: CreateCollectionRequest, header: ServiceHeader)( implicit reads: Reads[CreateCollectionResponse], writes: Writes[CreateCollectionRequest]): TaskService[ ServiceClientResponse[CreateCollectionResponse]] = serviceClient.post[CreateCollectionRequest, CreateCollectionResponse]( path = collectionsPath, headers = createHeaders(collectionsPath, header), body = request, reads = Some(reads)) def updateCollection( publicIdentifier: String, request: UpdateCollectionRequest, header: ServiceHeader)( implicit reads: Reads[UpdateCollectionResponse], writes: Writes[UpdateCollectionRequest]): TaskService[ ServiceClientResponse[UpdateCollectionResponse]] = { val path = s"$collectionsPath/$publicIdentifier" serviceClient.put[UpdateCollectionRequest, UpdateCollectionResponse]( path = path, headers = createHeaders(path, header), body = request, reads = Some(reads)) } def getCollection(publicIdentifier: String, header: ServiceMarketHeader)( implicit reads: Reads[Collection]): TaskService[ServiceClientResponse[Collection]] = { val path = s"$collectionsPath/$publicIdentifier" serviceClient .get[Collection](path = path, headers = createHeaders(path, header), reads = Some(reads)) } def getCollections(header: ServiceMarketHeader)( implicit reads: Reads[CollectionsResponse]): TaskService[ ServiceClientResponse[CollectionsResponse]] = serviceClient.get[CollectionsResponse]( path = collectionsPath, headers = createHeaders(collectionsPath, header), reads = Some(reads)) def categorize(request: CategorizeRequest, header: ServiceMarketHeader)( implicit reads: Reads[CategorizeResponse], writes: Writes[CategorizeRequest]): TaskService[ServiceClientResponse[CategorizeResponse]] = serviceClient.post[CategorizeRequest, CategorizeResponse]( path = categorizePath, headers = createHeaders(categorizePath, header), body = request, reads = Some(reads)) def categorizeDetail(request: CategorizeRequest, header: ServiceMarketHeader)( implicit reads: Reads[CategorizeDetailResponse], writes: Writes[CategorizeRequest]): TaskService[ ServiceClientResponse[CategorizeDetailResponse]] = serviceClient.post[CategorizeRequest, CategorizeDetailResponse]( path = categorizeDetailPath, headers = createHeaders(categorizeDetailPath, header), body = request, reads = Some(reads)) def recommendations( category: String, filter: Option[String], request: RecommendationsRequest, header: ServiceMarketHeader)( implicit reads: Reads[RecommendationsResponse], writes: Writes[RecommendationsRequest]): TaskService[ ServiceClientResponse[RecommendationsResponse]] = { val path = filter match { case Some(f) => s"$recommendationsPath/$category/$f" case _ => s"$recommendationsPath/$category" } serviceClient.post[RecommendationsRequest, RecommendationsResponse]( path = path, headers = createHeaders(path, header), body = request, reads = Some(reads)) } def recommendationsByApps(request: RecommendationsByAppsRequest, header: ServiceMarketHeader)( implicit reads: Reads[RecommendationsByAppsResponse], writes: Writes[RecommendationsByAppsRequest]): TaskService[ ServiceClientResponse[RecommendationsByAppsResponse]] = serviceClient.post[RecommendationsByAppsRequest, RecommendationsByAppsResponse]( path = recommendationsPath, headers = createHeaders(recommendationsPath, header), body = request, reads = Some(reads)) def getSubscriptions(header: ServiceHeader)( implicit reads: Reads[SubscriptionsResponse]): TaskService[ ServiceClientResponse[SubscriptionsResponse]] = serviceClient.get[SubscriptionsResponse]( path = subscriptionsPath, headers = createHeaders(subscriptionsPath, header), reads = Some(reads)) def subscribe( publicIdentifier: String, header: ServiceHeader): TaskService[ServiceClientResponse[Unit]] = { val path = s"$subscriptionsPath/$publicIdentifier" serviceClient.emptyPut( path = path, headers = createHeaders(path, header), reads = None, emptyResponse = true) } def unsubscribe( publicIdentifier: String, header: ServiceHeader): TaskService[ServiceClientResponse[Unit]] = { val path = s"$subscriptionsPath/$publicIdentifier" serviceClient.delete( path = path, headers = createHeaders(path, header), reads = None, emptyResponse = true) } def updateViewShareCollection( publicIdentifier: String, header: ServiceHeader): TaskService[ServiceClientResponse[Unit]] = { val path = s"$collectionsPath/$publicIdentifier$viewsPath" serviceClient.emptyPost( path = path, headers = createHeaders(path, header), reads = None, emptyResponse = true ) } def rankApps(request: RankAppsRequest, header: ServiceHeader)( implicit reads: Reads[RankAppsResponse], writes: Writes[RankAppsRequest]): TaskService[ServiceClientResponse[RankAppsResponse]] = serviceClient.post[RankAppsRequest, RankAppsResponse]( path = rankPath, headers = createHeaders(rankPath, header), body = request, reads = Some(reads)) def rankAppsByMoment(request: RankAppsByMomentRequest, header: ServiceHeader)( implicit reads: Reads[RankAppsByMomentResponse], writes: Writes[RankAppsByMomentRequest]): TaskService[ ServiceClientResponse[RankAppsByMomentResponse]] = serviceClient.post[RankAppsByMomentRequest, RankAppsByMomentResponse]( path = rankAppsByMomentPath, headers = createHeaders(rankAppsByMomentPath, header), body = request, reads = Some(reads)) def rankWidgetsByMoment(request: RankWidgetsByMomentRequest, header: ServiceHeader)( implicit reads: Reads[RankWidgetsByMomentResponse], writes: Writes[RankWidgetsByMomentRequest]): TaskService[ ServiceClientResponse[RankWidgetsByMomentResponse]] = serviceClient.post[RankWidgetsByMomentRequest, RankWidgetsByMomentResponse]( path = rankWidgetsByMomentPath, headers = createHeaders(rankWidgetsByMomentPath, header), body = request, reads = Some(reads)) def search(request: SearchRequest, header: ServiceMarketHeader)( implicit reads: Reads[SearchResponse], writes: Writes[SearchRequest]): TaskService[ServiceClientResponse[SearchResponse]] = serviceClient.post[SearchRequest, SearchResponse]( path = searchPath, headers = createHeaders(searchPath, header), body = request, reads = Some(reads)) private[this] def createHeaders[T <: BaseServiceHeader]( path: String, header: T): Seq[(String, String)] = { def readAndroidMarketToken: Option[String] = header match { case h: ServiceMarketHeader => h.androidMarketToken case _ => None } val algorithm = "HmacSHA512" val charset = "UTF-8" def hashMac(apiKey: String, url: String): String = { val mac = Mac.getInstance(algorithm) val secret = new SecretKeySpec(apiKey.getBytes(charset), algorithm) mac.init(secret) val bytesResult = mac.doFinal(url.getBytes(charset)) bytesResult.map("%02x".format(_)).mkString } Seq( (headerContentType, headerContentTypeValue), (headerAuthToken, hashMac(header.apiKey, serviceClient.baseUrl.concat(path))), (headerSessionToken, header.sessionToken), (headerAndroidId, header.androidId), (headerMarketLocalization, headerMarketLocalizationValue)) ++ (readAndroidMarketToken map ((headerAndroidMarketToken, _))).toSeq } } ================================================ FILE: modules/api/src/main/scala/cards/nine/api/version2/JsonImplicits.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version2 object JsonImplicits { import play.api.libs.json._ implicit val loginResponseReads = Json.reads[ApiLoginResponse] implicit val installationResponseReads = Json.reads[InstallationResponse] implicit val collectionAppReads = Json.reads[CollectionApp] implicit val collectionReads = Json.reads[Collection] implicit val collectionsResponseReads = Json.reads[CollectionsResponse] implicit val packagesStatsReads = Json.reads[PackagesStats] implicit val createCollectionResponseReads = Json.reads[CreateCollectionResponse] implicit val updateCollectionResponseReads = Json.reads[UpdateCollectionResponse] implicit val categorizedAppReads = Json.reads[CategorizedApp] implicit val categorizedAppDetailReads = Json.reads[CategorizedAppDetail] implicit val categorizeResponseReads = Json.reads[CategorizeResponse] implicit val categorizeDetailResponseReads = Json.reads[CategorizeDetailResponse] implicit val recommendationAppReads = Json.reads[NotCategorizedApp] implicit val recommendationsResponseReads = Json.reads[RecommendationsResponse] implicit val recommendationsByAppsResponseReads = Json.reads[RecommendationsByAppsResponse] implicit val subscriptionsResponseReads = Json.reads[SubscriptionsResponse] implicit val rankAppsCategoryResponseReads = Json.reads[RankAppsCategoryResponse] implicit val rankAppsResponseReads = Json.reads[RankAppsResponse] implicit val rankAppsByMomentResponseReads = Json.reads[RankAppsByMomentResponse] implicit val rankWidgetsResponseReads = Json.reads[RankWidgetsResponse] implicit val rankWidgetsWithMomentResponse = Json.reads[RankWidgetsWithMomentResponse] implicit val rankWidgetsByMomentResponse = Json.reads[RankWidgetsByMomentResponse] implicit val searchResponseReads = Json.reads[SearchResponse] implicit val loginRequestWrites = Json.writes[ApiLoginRequest] implicit val installationRequestWrites = Json.writes[InstallationRequest] implicit val createCollectionRequestWrites = Json.writes[CreateCollectionRequest] implicit val collectionUpdateInfoWrites = Json.writes[CollectionUpdateInfo] implicit val updateCollectionRequestWrites = Json.writes[UpdateCollectionRequest] implicit val categorizeRequestWrites = Json.writes[CategorizeRequest] implicit val recommendationsRequestWrites = Json.writes[RecommendationsRequest] implicit val recommendationsByAppsRequestWrites = Json.writes[RecommendationsByAppsRequest] implicit val rankAppsRequestWrites = Json.writes[RankAppsRequest] implicit val rankAppsByMomentRequestWrites = Json.writes[RankAppsByMomentRequest] implicit val rankWidgetsByMomentRequest = Json.writes[RankWidgetsByMomentRequest] implicit val searchRequestWrites = Json.writes[SearchRequest] } ================================================ FILE: modules/api/src/main/scala/cards/nine/api/version2/Model.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version2 trait BaseServiceHeader { def apiKey: String def sessionToken: String def androidId: String } case class ServiceHeader(apiKey: String, sessionToken: String, androidId: String) extends BaseServiceHeader case class ServiceMarketHeader( apiKey: String, sessionToken: String, androidId: String, androidMarketToken: Option[String]) extends BaseServiceHeader case class ApiLoginRequest(email: String, androidId: String, tokenId: String) case class ApiLoginResponse(apiKey: String, sessionToken: String) case class InstallationRequest(deviceToken: String) case class InstallationResponse(androidId: String, deviceToken: String) case class CollectionsResponse(collections: Seq[Collection]) case class CreateCollectionRequest( name: String, author: String, icon: String, category: String, community: Boolean, packages: Seq[String]) case class CreateCollectionResponse(publicIdentifier: String, packagesStats: PackagesStats) case class UpdateCollectionRequest( collectionInfo: Option[CollectionUpdateInfo], packages: Option[Seq[String]]) case class UpdateCollectionResponse(publicIdentifier: String, packagesStats: PackagesStats) case class CategorizeRequest(items: Seq[String]) case class CategorizeResponse(errors: Seq[String], items: Seq[CategorizedApp]) case class CategorizeDetailResponse(errors: Seq[String], items: Seq[CategorizedAppDetail]) case class RecommendationsRequest(excludePackages: Seq[String], limit: Int) case class RecommendationsResponse(items: Seq[NotCategorizedApp]) case class RecommendationsByAppsRequest( packages: Seq[String], excludePackages: Seq[String], limit: Int) case class RecommendationsByAppsResponse(apps: Seq[NotCategorizedApp]) case class SubscriptionsResponse(subscriptions: Seq[String]) case class RankAppsRequest(items: Map[String, Seq[String]], location: Option[String]) case class RankAppsResponse(items: Seq[RankAppsCategoryResponse]) case class RankAppsCategoryResponse(category: String, packages: Seq[String]) case class SearchRequest(query: String, excludePackages: Seq[String], limit: Int) case class SearchResponse(items: Seq[NotCategorizedApp]) case class RankAppsByMomentRequest( items: Seq[String], moments: Seq[String], location: Option[String], limit: Int) case class RankAppsByMomentResponse(items: Seq[RankAppsCategoryResponse]) case class RankWidgetsResponse(packageName: String, className: String) case class RankWidgetsByMomentRequest( items: Seq[String], moments: Seq[String], location: Option[String], limit: Int) case class RankWidgetsWithMomentResponse(moment: String, widgets: Seq[RankWidgetsResponse]) case class RankWidgetsByMomentResponse(items: Seq[RankWidgetsWithMomentResponse]) case class PackagesStats(added: Int, removed: Option[Int] = None) case class Collection( name: String, author: String, owned: Boolean, icon: String, category: String, community: Boolean, publishedOn: String, installations: Option[Int], views: Option[Int], subscriptions: Option[Int], publicIdentifier: String, appsInfo: Seq[CollectionApp], packages: Seq[String]) case class CollectionApp( stars: Double, icon: String, packageName: String, downloads: String, categories: Seq[String], title: String, free: Boolean) case class CollectionUpdateInfo(title: String) case class CategorizedApp(packageName: String, categories: Seq[String]) case class CategorizedAppDetail( packageName: String, title: String, categories: Seq[String], icon: String, free: Boolean, downloads: String, stars: Double) case class NotCategorizedApp( packageName: String, title: String, downloads: String, icon: String, stars: Double, free: Boolean, screenshots: Seq[String]) ================================================ FILE: modules/api/src/test/scala/cards/nine/api/rest/client/Messages.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client case class SampleRequest(message: String) case class SampleResponse(message: String) ================================================ FILE: modules/api/src/test/scala/cards/nine/api/rest/client/ServiceClientData.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client import cards.nine.api.rest.client.http.{HttpClientException, HttpClientResponse} import play.api.libs.json.Json trait ServiceClientData { case class SampleRequest(message: String) case class SampleResponse(message: String) val baseUrl = "http://sampleUrl" val path = "/myPath" val headers = Seq(("header1", "value1"), ("header2", "value2")) implicit val readsResponse = Json.reads[SampleResponse] implicit val writesRequest = Json.writes[SampleRequest] val message = "Hello World!" val json = s"""{ "message" : "$message" }""" val invalidJson = s"""{ "unknownProperty" : false }""" val sampleRequest = SampleRequest("sample-request") val sampleResponse = Some(SampleResponse(message)) val statusCodeOk = 200 val statusCodeNotFound = 404 val validHttpClientResponse = HttpClientResponse(statusCodeOk, Some(json)) val validEmptyHttpClientResponse = HttpClientResponse(statusCodeOk, None) val notFoundHttpClientResponse = HttpClientResponse(statusCodeNotFound, None) val invalidHttpClientResponse = HttpClientResponse(statusCodeOk, Some(invalidJson)) val exception = HttpClientException("") } ================================================ FILE: modules/api/src/test/scala/cards/nine/api/rest/client/ServiceClientSpec.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client import cards.nine.commons.services.TaskService import cards.nine.api.rest.client.http.{HttpClient, HttpClientException, HttpClientResponse} import org.hamcrest.core.IsEqual import org.specs2.mock.Mockito import org.specs2.mutable.Specification import org.specs2.specification.Scope import play.api.libs.json.Json import cards.nine.commons.test.TaskServiceTestOps._ import monix.eval.Task import cats.syntax.either._ import cards.nine.commons.test.TaskServiceSpecification trait ServiceClientSpecification extends TaskServiceSpecification with Mockito with ServiceClientData { trait ServiceClientScope extends Scope { val httpClient = mock[HttpClient] val serviceClient = new ServiceClient(httpClient, baseUrl) } } case class Test(value: Int) class ServiceClientSpec extends ServiceClientSpecification { "get method from ServiceClient" should { "return a valid response when service return a valid response" in new ServiceClientScope { httpClient.doGet(any, any) returns serviceRight(validHttpClientResponse) serviceClient.get(path, headers, Some(readsResponse), emptyResponse = false) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data shouldEqual sampleResponse } there was one(httpClient).doGet(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return None when the service return a valid empty response" in new ServiceClientScope { httpClient.doGet(any, any) returns serviceRight(validHttpClientResponse) serviceClient.get[Unit](path, headers, None, emptyResponse = true) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beNone } there was one(httpClient).doGet(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an exception" in new ServiceClientScope { httpClient.doGet(any, any) returns serviceLeft(exception) serviceClient .get(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doGet(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a status code of 'Not Found'" in new ServiceClientScope { httpClient.doGet(any, any) returns serviceRight(notFoundHttpClientResponse) serviceClient .get(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doGet(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a invalid JSON in the response" in new ServiceClientScope { httpClient.doGet(any, any) returns serviceRight(invalidHttpClientResponse) serviceClient .get(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doGet(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an empty response but we're expecting some" in new ServiceClientScope { httpClient.doGet(any, any) returns serviceRight(validEmptyHttpClientResponse) serviceClient .get(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doGet(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a valid response but the JSON reads is not provided'" in new ServiceClientScope { httpClient.doGet(any, any) returns serviceRight(validHttpClientResponse) serviceClient .get(path, headers, None, emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doGet(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } } "delete method from ServiceClient" should { "return a valid response when service return a valid response" in new ServiceClientScope { httpClient.doDelete(any, any) returns serviceRight(validHttpClientResponse) serviceClient.delete(path, headers, Some(readsResponse), emptyResponse = false) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data shouldEqual sampleResponse } there was one(httpClient).doDelete(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return None when the service return a valid empty response" in new ServiceClientScope { httpClient.doDelete(any, any) returns serviceRight(validHttpClientResponse) serviceClient.delete[Unit](path, headers, None, emptyResponse = true) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beNone } there was one(httpClient).doDelete(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an exception" in new ServiceClientScope { httpClient.doDelete(any, any) returns serviceLeft(exception) serviceClient .delete(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doDelete(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a status code of 'Not Found'" in new ServiceClientScope { httpClient.doDelete(any, any) returns serviceRight(notFoundHttpClientResponse) serviceClient .delete(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doDelete(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a invalid JSON in the response" in new ServiceClientScope { httpClient.doDelete(any, any) returns serviceRight(invalidHttpClientResponse) serviceClient .delete(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doDelete(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an empty response but we're expecting some" in new ServiceClientScope { httpClient.doDelete(any, any) returns serviceRight(validEmptyHttpClientResponse) serviceClient .delete(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doDelete(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a valid response but the JSON reads is not provided'" in new ServiceClientScope { httpClient.doDelete(any, any) returns serviceRight(validHttpClientResponse) serviceClient .delete(path, headers, None, emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doDelete(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } } "emptyPost method from ServiceClient" should { "return a valid response when service return a valid response" in new ServiceClientScope { httpClient.doPost(any, any) returns serviceRight(validHttpClientResponse) serviceClient .emptyPost(path, headers, Some(readsResponse), emptyResponse = false) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data shouldEqual sampleResponse } there was one(httpClient).doPost(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return None when the service return a valid empty response" in new ServiceClientScope { httpClient.doPost(any, any) returns serviceRight(validHttpClientResponse) serviceClient.emptyPost[Unit](path, headers, None, emptyResponse = true) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beNone } there was one(httpClient).doPost(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an exception" in new ServiceClientScope { httpClient.doPost(any, any) returns serviceLeft(exception) serviceClient .emptyPost(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a status code of 'Not Found'" in new ServiceClientScope { httpClient.doPost(any, any) returns serviceRight(notFoundHttpClientResponse) serviceClient .emptyPost(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a invalid JSON in the response" in new ServiceClientScope { httpClient.doPost(any, any) returns serviceRight(invalidHttpClientResponse) serviceClient .emptyPost(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an empty response but we're expecting some" in new ServiceClientScope { httpClient.doPost(any, any) returns serviceRight(validEmptyHttpClientResponse) serviceClient .emptyPost(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a valid response but the JSON reads is not provided'" in new ServiceClientScope { httpClient.doPost(any, any) returns serviceRight(validHttpClientResponse) serviceClient .emptyPost(path, headers, None, emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } } "post method from ServiceClient" should { "return a valid response when service return a valid response" in new ServiceClientScope { httpClient.doPost(any, any, any)(any) returns serviceRight(validHttpClientResponse) serviceClient.post( path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data shouldEqual sampleResponse } there was one(httpClient).doPost(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return None when the service return a valid empty response" in new ServiceClientScope { httpClient.doPost(any, any, any)(any) returns serviceRight(validHttpClientResponse) serviceClient.post[SampleRequest, Unit]( path, headers, sampleRequest, None, emptyResponse = true) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beNone } there was one(httpClient).doPost(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an exception" in new ServiceClientScope { httpClient.doPost(any, any, any)(any) returns serviceLeft(exception) serviceClient .post(path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a status code of 'Not Found'" in new ServiceClientScope { httpClient.doPost(any, any, any)(any) returns serviceRight(notFoundHttpClientResponse) serviceClient .post(path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a invalid JSON in the response" in new ServiceClientScope { httpClient.doPost(any, any, any)(any) returns serviceRight(invalidHttpClientResponse) serviceClient .post(path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an empty response but we're expecting some" in new ServiceClientScope { httpClient.doPost(any, any, any)(any) returns serviceRight(validEmptyHttpClientResponse) serviceClient .post(path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a valid response but the JSON reads is not provided'" in new ServiceClientScope { httpClient.doPost(any, any, any)(any) returns serviceRight(validHttpClientResponse) serviceClient .post(path, headers, sampleRequest, None, emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPost(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } } "emptyPut method from ServiceClient" should { "return a valid response when service return a valid response" in new ServiceClientScope { httpClient.doPut(any, any) returns serviceRight(validHttpClientResponse) serviceClient .emptyPut(path, headers, Some(readsResponse), emptyResponse = false) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data shouldEqual sampleResponse } there was one(httpClient).doPut(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return None when the service return a valid empty response" in new ServiceClientScope { httpClient.doPut(any, any) returns serviceRight(validHttpClientResponse) serviceClient.emptyPut[Unit](path, headers, None, emptyResponse = true) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beNone } there was one(httpClient).doPut(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an exception" in new ServiceClientScope { httpClient.doPut(any, any) returns serviceLeft(exception) serviceClient .emptyPut(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a status code of 'Not Found'" in new ServiceClientScope { httpClient.doPut(any, any) returns serviceRight(notFoundHttpClientResponse) serviceClient .emptyPut(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a invalid JSON in the response" in new ServiceClientScope { httpClient.doPut(any, any) returns serviceRight(invalidHttpClientResponse) serviceClient .emptyPut(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an empty response but we're expecting some" in new ServiceClientScope { httpClient.doPut(any, any) returns serviceRight(validEmptyHttpClientResponse) serviceClient .emptyPut(path, headers, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a valid response but the JSON reads is not provided'" in new ServiceClientScope { httpClient.doPut(any, any) returns serviceRight(validHttpClientResponse) serviceClient .emptyPut(path, headers, None, emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers) there was noMoreCallsTo(httpClient) } } "put method from ServiceClient" should { "return a valid response when service return a valid response" in new ServiceClientScope { httpClient.doPut(any, any, any)(any) returns serviceRight(validHttpClientResponse) serviceClient .put(path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data shouldEqual sampleResponse } there was one(httpClient).doPut(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return None when the service return a valid empty response" in new ServiceClientScope { httpClient.doPut(any, any, any)(any) returns serviceRight(validHttpClientResponse) serviceClient.put[SampleRequest, Unit]( path, headers, sampleRequest, None, emptyResponse = true) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beNone } there was one(httpClient).doPut(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an exception" in new ServiceClientScope { httpClient.doPut(any, any, any)(any) returns serviceLeft(exception) serviceClient .put(path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a status code of 'Not Found'" in new ServiceClientScope { httpClient.doPut(any, any, any)(any) returns serviceRight(notFoundHttpClientResponse) serviceClient .put(path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a invalid JSON in the response" in new ServiceClientScope { httpClient.doPut(any, any, any)(any) returns serviceRight(invalidHttpClientResponse) serviceClient .put(path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return an empty response but we're expecting some" in new ServiceClientScope { httpClient.doPut(any, any, any)(any) returns serviceRight(validEmptyHttpClientResponse) serviceClient .put(path, headers, sampleRequest, Some(readsResponse), emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } "return ServiceClientException when the service return a valid response but the JSON reads is not provided'" in new ServiceClientScope { httpClient.doPut(any, any, any)(any) returns serviceRight(validHttpClientResponse) serviceClient .put(path, headers, sampleRequest, None, emptyResponse = false) .mustLeft[ServiceClientException] there was one(httpClient).doPut(s"$baseUrl$path", headers, sampleRequest)(writesRequest) there was noMoreCallsTo(httpClient) } } } ================================================ FILE: modules/api/src/test/scala/cards/nine/api/rest/client/http/OkHttpClientSpec.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.rest.client.http import cards.nine.commons.test.TaskServiceSpecification import cards.nine.api.rest.client.SampleRequest import okhttp3.Request import org.specs2.matcher.DisjunctionMatchers import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.libs.json.Json trait OkHttpClientSpecification extends TaskServiceSpecification with DisjunctionMatchers with Mockito { trait OkHttpClientScope extends Scope { val baseUrl = "http://sampleUrl" implicit val readsRequest = Json.reads[SampleRequest] implicit val writesRequest = Json.writes[SampleRequest] val acceptedMethod: Option[String] = None val acceptedBody: Option[SampleRequest] = None val acceptedHeaders = Seq(("header1", "value1"), ("header2", "value2")) val request = new okhttp3.Request.Builder().url(baseUrl).build() val statusCode = 200 val message = "Hello World!" val json = s"""{ "message" : "$message" }""" val baseException = new IllegalArgumentException("") val okHttpResponse = new okhttp3.Response.Builder() .protocol(okhttp3.Protocol.HTTP_1_1) .request(request) .code(statusCode) .message("Alright") .body(okhttp3.ResponseBody.create(okhttp3.MediaType.parse("application/json"), json)) .build() private def isValidMethod(req: okhttp3.Request): Boolean = acceptedMethod.getOrElse(req.method) == req.method private def isValidBody(req: okhttp3.Request): Boolean = acceptedBody match { case Some(r) => val buffer = new okio.Buffer() req.body().writeTo(buffer) Json.parse(buffer.readUtf8()).as[SampleRequest](readsRequest) == r case _ => true } private def isValidHeaders(req: okhttp3.Request): Boolean = { val headers = 0 until req.headers().size() map { index => val name = req.headers().name(index) (name, req.header(name)) } headers == acceptedHeaders } val okHttpClient = new OkHttpClient(new okhttp3.OkHttpClient() { override def newCall(theRequest: okhttp3.Request): okhttp3.Call = { new okhttp3.Call { override def request(): Request = theRequest override def cancel(): Unit = {} override def isCanceled: Boolean = false override def isExecuted: Boolean = false override def enqueue(responseCallback: okhttp3.Callback): Unit = {} override def execute(): okhttp3.Response = { if (isValidMethod(theRequest) && isValidHeaders(theRequest) && isValidBody(theRequest)) okHttpResponse else throw baseException } } } }) } } class OkHttpClientSpec extends OkHttpClientSpecification { "OkHttpClient component" should { "return the response for a successfully get request" in new OkHttpClientScope { override val acceptedMethod = Some(Methods.GET.toString) val response = okHttpClient.doGet(baseUrl, acceptedHeaders).run response shouldEqual Right(HttpClientResponse(statusCode, Some(json))) } "return the response for a successfully delete request" in new OkHttpClientScope { override val acceptedMethod = Some(Methods.DELETE.toString) val response = okHttpClient.doDelete(baseUrl, acceptedHeaders).run response shouldEqual Right(HttpClientResponse(statusCode, Some(json))) } "return the response for a successfully empty post request" in new OkHttpClientScope { override val acceptedMethod = Some(Methods.POST.toString) val response = okHttpClient.doPost(baseUrl, acceptedHeaders).run response shouldEqual Right(HttpClientResponse(statusCode, Some(json))) } "return the response for a successfully post request" in new OkHttpClientScope { override val acceptedMethod = Some(Methods.POST.toString) val sampleRequest = SampleRequest("request") override val acceptedBody = Some(sampleRequest) val response = okHttpClient.doPost[SampleRequest](baseUrl, acceptedHeaders, sampleRequest).run response shouldEqual Right(HttpClientResponse(statusCode, Some(json))) } "return the response for a successfully empty put request" in new OkHttpClientScope { override val acceptedMethod = Some(Methods.PUT.toString) val response = okHttpClient.doPut(baseUrl, acceptedHeaders).run response shouldEqual Right(HttpClientResponse(statusCode, Some(json))) } "return the response for a successfully put request" in new OkHttpClientScope { override val acceptedMethod = Some(Methods.PUT.toString) val sampleRequest = SampleRequest("request") override val acceptedBody = Some(sampleRequest) val response = okHttpClient.doPut[SampleRequest](baseUrl, acceptedHeaders, sampleRequest).run response shouldEqual Right(HttpClientResponse(statusCode, Some(json))) } "return an Exception for an unexpected method" in new OkHttpClientScope { override val acceptedMethod = Some(Methods.GET.toString) val response = okHttpClient.doDelete(baseUrl, Seq.empty).run response must beAnInstanceOf[Left[IllegalArgumentException, _]] } "return an Exception for an unexpected request" in new OkHttpClientScope { override val acceptedMethod = Some(Methods.POST.toString) override val acceptedBody = Some(SampleRequest("request")) val response = okHttpClient.doPut[SampleRequest](baseUrl, Seq.empty, SampleRequest("bad_request")).run response must beAnInstanceOf[Left[IllegalArgumentException, _]] } } } ================================================ FILE: modules/api/src/test/scala/cards/nine/api/version1/ApiServiceData.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version1 import play.api.libs.json.JsNull trait ApiServiceData { val baseUrl = "http://localhost:8080" val statusCodeOk = 200 val userId = 10 val sessionToken = "session-token" val username = "username" val password = "password" val email = "email" val deviceId = "device-id" val deviceName = "Nexus 47" val secretToken = "secret-token" val permission = "androidmarket" val headers = Seq( ("header-1", "header-1-value"), ("header-2", "header-2-value"), ("header-3", "header-3-value")) val authGoogleDevice = AuthGoogleDevice( name = deviceName, deviceId = deviceId, secretToken = secretToken, permissions = Seq(permission)) val authGoogle = AuthGoogle(email = email, devices = Seq(authGoogleDevice)) val authData = AuthData(google = Some(authGoogle), facebook = None, twitter = None, anonymous = None) val emptyUser = User(None, None, None, None, None, None) val user = User( Some(userId.toString), Some(sessionToken), Some(username), Some(password), Some(email), authData = Some(authData)) val userConfigProfileImage = UserConfigProfileImage(imageType = 0, imageUrl = "http://fakeUrl", secureUrl = None) val userConfigPlusProfile = UserConfigPlusProfile(username, userConfigProfileImage) val userConfigCollectionItem = UserConfigCollectionItem( itemType = "item type", title = "item title", metadata = JsNull, categories = None) val userConfigCollection = UserConfigCollection( name = "collection name", originalSharedCollectionId = Some("original collection id"), sharedCollectionId = Some("collection id"), sharedCollectionSubscribed = Some(true), items = Seq(userConfigCollectionItem), collectionType = "collection type", constrains = Seq.empty, wifi = Seq.empty, occurrence = Seq.empty, icon = "social", radius = 0, lat = 0, lng = 0, alt = 0, category = Some("SOCIAL")) val userConfigDevice = UserConfigDevice(deviceId, deviceName, Seq(userConfigCollection)) val userConfigGeoInfo = UserConfigGeoInfo(None, None, None, None) val userConfigStatusInfo = UserConfigStatusInfo( products = Seq.empty, friendsReferred = 10, themesShared = 5, collectionsShared = 5, customCollections = 2, earlyAdopter = false, communityMember = true, joinedThrough = None, tester = false) val userConfig = UserConfig( userId.toString, email, userConfigPlusProfile, Seq(userConfigDevice), userConfigGeoInfo, userConfigStatusInfo) } ================================================ FILE: modules/api/src/test/scala/cards/nine/api/version1/ApiServiceSpec.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version1 import cats.syntax.either._ import cards.nine.commons.services.TaskService import cards.nine.commons.test.TaskServiceSpecification import cards.nine.api.rest.client.ServiceClient import cards.nine.api.rest.client.messages.ServiceClientResponse import monix.eval.Task import org.specs2.mock.Mockito import org.specs2.specification.Scope trait ApiServiceSpecification extends TaskServiceSpecification with Mockito with ApiServiceData { trait ApiServiceScope extends Scope { val mockedServiceClient = mock[ServiceClient] mockedServiceClient.baseUrl returns baseUrl val apiService = new ApiService(mockedServiceClient) } } class ApiServiceSpec extends ApiServiceSpecification { import JsonImplicits._ "login" should { "return the status code and the response" in new ApiServiceScope { mockedServiceClient.post[User, User](any, any, any, any, any)(any) returns TaskService(Task(Either.right(ServiceClientResponse(statusCodeOk, Some(user))))) apiService.login(emptyUser, headers) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(user) } there was one(mockedServiceClient).post( path = "/users", headers = headers, body = emptyUser, reads = Some(userReads), emptyResponse = false)(userWrites) } } "getUserConfig" should { "return the status code and the response" in new ApiServiceScope { mockedServiceClient.get[UserConfig](any, any, any, any) returns TaskService(Task(Either.right(ServiceClientResponse(statusCodeOk, Some(userConfig))))) apiService.getUserConfig(headers) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(userConfig) } there was one(mockedServiceClient).get( path = "/ninecards/userconfig", headers = headers, reads = Some(userConfigReads), emptyResponse = false) } } } ================================================ FILE: modules/api/src/test/scala/cards/nine/api/version2/ApiServiceData.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version2 import scala.util.Random trait ApiServiceData { val baseUrl = "http://localhost:8080" val headerContentType = "Content-Type" val headerContentTypeValue = "application/json" val headerAuthToken = "X-Auth-Token" val headerSessionToken = "X-Session-Token" val headerAndroidId = "X-Android-ID" val headerMarketLocalization = "X-Android-Market-Localization" val headerMarketLocalizationValue = "en-US" val headerAndroidMarketToken = "X-Google-Play-Token" val statusCodeOk = 200 val email = "email@dot.com" val loginId = "login-id" val tokenId = "token-id" val apiKey = "api-key" val deviceToken = "device-token" val sessionToken = "session-token" val androidId = "android-id" val marketToken = "market-token" val installationAuthToken = "f9dfeb34c16dae2715dc85b82f7d2a91ff3999212ed5235950d3328c0ab8e35886c98a788ed5fa35211be0a2b7829306b83c7cc4c954025d2b69786608666785" val latestCollectionsAuthToken = "eeec8eee60eba7e312cfc752396c0f05625bc372c95e1c6e70ea0e02b61f23e1f238b42c133fedcba2eb725c3e6a14542c2070d8db40e7ffa5a74eb8f50b8f60" val topCollectionsAuthToken = "7d7290b7c03da9cf5000b05c148ed430bb333c67a47d57085438481e05f683428b9ad662309fdb637f925bd1eb85b8eefcd522b206314977028f29724ca922d2" val collectionsAuthToken = "c5284c3a5ff576a984689bd13ff3e6144dc5ebfedcd14f4eb7ba5b8b8f5ff1211b677fdf12ff8fe93d226de1fb45c706578c4ee43d47dda5cde51c91a7c5bb06" val collectionsIdAuthToken = "5fdf0285acf5b1f0223c903553558a7512c92aabac6e6c2dd1daa794682966f954438645ce6cd139036ace029e38c609a23aaeabcdd453d25a5349d8b5cf178c" val categorizeAuthToken = "4f129e296588493aab55e0192894ed95867546674844479f8dce0f0f506eed80991a1f09d459d476335acdf27e3f178d16f8df94c45a0e77d4f10935f8199493" val categorizeDetailAuthToken = "f6ccbb142c90bb3b527ee2d27867daf4324403255fe90f4f5e0b4b25f992fa7b0c4635a7161fc104f664ea7bea337afe1fffc3e12c4c597eeee56557d089d23b" val recommendationsAuthToken = "915f6db594dd9be2bc9cfb2a232c52bccee633afe97086cca7f468690ad8ca7bb87747b5ee13a35af0103091bf9b0d5a38f0cccd5179ca98b399c65f7afae6a2" val recommendationsByAppsAuthToken = "ee388db874ece41e4a446bb8c36f0967944d71d87239ae5d629ee6db074508e318eb70c134572d9a3d10192e07b11135360a539a302ca347f3bbdeec6970129b" val subscriptionsAuthToken = "b45ad90c3d18f43b7b921c2aeec3258a8a1adf11ad75e4c68928ac89855c5c019b921bc268bc3072a2735718479809a71810e684b11051619ed564392d5be3dd" val rankAppsAuthToken = "b105db467bb6d9087471aeeba4337bb8c7b54a383ddbe711dd6b36536c053ec0520ab3fb99dbe03471dd5a6a09001b80dd784ee16684cb71ac6c063926e5d109" val searchAuthToken = "1f6f5235a235a4edd3bf7108d1d898a1df219219290c1409d231e6213f47180d9be85e299019434ee9339da83ae28eb7a83dcd95464bf0edbb20cbc1b86506c4" val updateCollectionAuthToken = "5fc154fa279c6cb9350d502289e737e3a568ab7ceec6f3b97628644406c100337c88387b47b5ed894582cc01b35bfba156a0d224a5edc392ef23e15811b53d79" val serviceHeader = ServiceHeader(apiKey, sessionToken, androidId) val serviceMarketHeader = ServiceMarketHeader(apiKey, sessionToken, androidId, Some(marketToken)) def createHeaders(authToken: String) = Seq( (headerContentType, headerContentTypeValue), (headerAuthToken, authToken), (headerSessionToken, sessionToken), (headerAndroidId, androidId), (headerMarketLocalization, headerMarketLocalizationValue)) def createMarketHeaders(authToken: String) = createHeaders(authToken) :+ (headerAndroidMarketToken, marketToken) val category = "SOCIAL" val publicIdentifier = "collection-public-identifier" val offset = 0 val limit = 100 val collectionName = "collection name" val collectionAuthor = "collection author" val collectionIcon = "collection icon" val collectionApp = CollectionApp( stars = 3.5, icon = "app icon", packageName = "com.package.app", downloads = "500,000,000+", categories = Seq(category), title = "app title", free = true) val collection = Collection( name = collectionName, author = collectionAuthor, owned = false, icon = collectionIcon, category = category, community = true, publishedOn = "2016-08-16T14:55:30.574000", installations = Some(10), views = Some(100), subscriptions = Some(100), publicIdentifier = publicIdentifier, appsInfo = Seq(collectionApp), packages = Seq(collectionApp.packageName)) val createCollectionRequest = CreateCollectionRequest( name = collectionName, author = collectionAuthor, icon = collectionIcon, category = category, community = true, packages = Seq(collectionApp.packageName)) val createCollectionResponse = CreateCollectionResponse(publicIdentifier = publicIdentifier, packagesStats = PackagesStats(1)) val updateCollectionRequest = UpdateCollectionRequest( collectionInfo = Some(CollectionUpdateInfo(title = collectionName)), packages = Some(Seq(collectionApp.packageName))) val updateCollectionResponse = UpdateCollectionResponse(publicIdentifier = publicIdentifier, packagesStats = PackagesStats(1)) val categorizeRequest = CategorizeRequest(items = Seq(collectionApp.packageName)) val categorizeResponse = CategorizeResponse( errors = Seq.empty, items = Seq(CategorizedApp(packageName = collectionApp.packageName, categories = Seq(category)))) val categorizeDetailResponse = CategorizeDetailResponse( errors = Seq.empty, items = Seq( CategorizedAppDetail( packageName = collectionApp.packageName, title = collectionApp.title, categories = Seq(category), icon = collectionApp.icon, free = collectionApp.free, downloads = collectionApp.downloads, stars = collectionApp.stars))) val notCategorizedApp = NotCategorizedApp( packageName = collectionApp.packageName, title = collectionApp.title, downloads = collectionApp.downloads, icon = collectionApp.icon, stars = collectionApp.stars, free = collectionApp.free, screenshots = Seq("screenshot1", "screenshot2", "screenshot3")) val recommendationsRequest = RecommendationsRequest(excludePackages = Seq("com.package.sample"), limit = 10) val recommendationsByAppsRequest = RecommendationsByAppsRequest( packages = Seq("com.fortysevendeg.ninecardslauncher"), excludePackages = Seq("com.package.sample"), limit = 10) val recommendationsResponse = RecommendationsResponse(items = Seq(notCategorizedApp)) val recommendationsByAppsResponse = RecommendationsByAppsResponse(apps = Seq(notCategorizedApp)) val subscriptions = Seq(publicIdentifier) val packages = Seq("com.package.sample.second", "com.package.sample.third", "com.package.sample.first") val packagesOrdered = Seq("com.package.sample.first", "com.package.sample.second", "com.package.sample.third") val items = Map(category -> packages) val location = Random.nextBoolean match { case false => None case true => Some("ES") } val rankAppsRequest = RankAppsRequest(items = items, location = location) val rankAppsResponse = RankAppsResponse( Seq(RankAppsCategoryResponse(category = category, packages = packagesOrdered))) val searchRequest = SearchRequest(query = "sample query", excludePackages = Seq("com.package.sample"), limit = 10) val searchResponse = SearchResponse(items = Seq(notCategorizedApp)) } ================================================ FILE: modules/api/src/test/scala/cards/nine/api/version2/ApiServiceSpec.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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 cards.nine.api.version2 import cards.nine.commons.test.TaskServiceSpecification import cards.nine.api.rest.client.ServiceClient import cards.nine.api.rest.client.messages.ServiceClientResponse import org.specs2.mock.Mockito import org.specs2.specification.Scope trait ApiServiceSpecification extends TaskServiceSpecification with Mockito with ApiServiceData { trait ApiServiceScope extends Scope { val mockedServiceClient = mock[ServiceClient] mockedServiceClient.baseUrl returns baseUrl val apiService = new ApiService(mockedServiceClient) } } class ApiServiceSpec extends ApiServiceSpecification { import JsonImplicits._ "login" should { "return the status code and the response" in new ApiServiceScope { val response = ApiLoginResponse(apiKey, sessionToken) mockedServiceClient.post[ApiLoginRequest, ApiLoginResponse](any, any, any, any, any)(any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(response))) val request = ApiLoginRequest(email, loginId, tokenId) apiService.login(request) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(response) } there was one(mockedServiceClient).post( path = "/login", headers = Seq((headerContentType, headerContentTypeValue)), body = request, reads = Some(loginResponseReads), emptyResponse = false)(loginRequestWrites) } } "installations" should { "return the status code and the response" in new ApiServiceScope { val response = InstallationResponse(androidId, deviceToken) mockedServiceClient.put[InstallationRequest, InstallationResponse](any, any, any, any, any)( any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(response))) val request = InstallationRequest(deviceToken) apiService.installations(request, serviceHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(response) } there was one(mockedServiceClient).put( path = "/installations", headers = createHeaders(installationAuthToken), body = request, reads = Some(installationResponseReads), emptyResponse = false)(installationRequestWrites) } "latest collections" should { "return the status code and the response" in new ApiServiceScope { val response = CollectionsResponse(Seq(collection)) mockedServiceClient.get[CollectionsResponse](any, any, any, any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(response))) apiService.latestCollections(category, offset, limit, serviceMarketHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(response) } there was one(mockedServiceClient).get( path = s"/collections/latest/$category/$offset/$limit", headers = createMarketHeaders(latestCollectionsAuthToken), reads = Some(collectionsResponseReads), emptyResponse = false) } } "top collections" should { "return the status code and the response" in new ApiServiceScope { val response = CollectionsResponse(Seq(collection)) mockedServiceClient.get[CollectionsResponse](any, any, any, any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(response))) apiService.topCollections(category, offset, limit, serviceMarketHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(response) } there was one(mockedServiceClient).get( path = s"/collections/top/$category/$offset/$limit", headers = createMarketHeaders(topCollectionsAuthToken), reads = Some(collectionsResponseReads), emptyResponse = false) } } "create collection" should { "return the status code and the response" in new ApiServiceScope { mockedServiceClient.post[CreateCollectionRequest, CreateCollectionResponse]( any, any, any, any, any)(any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(createCollectionResponse))) apiService.createCollection(createCollectionRequest, serviceHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(createCollectionResponse) } there was one(mockedServiceClient).post( path = "/collections", headers = createHeaders(collectionsAuthToken), body = createCollectionRequest, reads = Some(createCollectionResponseReads), emptyResponse = false)(createCollectionRequestWrites) } } "update collection" should { "return the status code and the response" in new ApiServiceScope { mockedServiceClient.put[UpdateCollectionRequest, UpdateCollectionResponse]( any, any, any, any, any)(any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(updateCollectionResponse))) apiService .updateCollection(publicIdentifier, updateCollectionRequest, serviceHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(updateCollectionResponse) } there was one(mockedServiceClient).put( path = s"/collections/$publicIdentifier", headers = createHeaders(collectionsIdAuthToken), body = updateCollectionRequest, reads = Some(updateCollectionResponseReads), emptyResponse = false)(updateCollectionRequestWrites) } } "get collection" should { "return the status code and the response" in new ApiServiceScope { val response = collection mockedServiceClient.get[Collection](any, any, any, any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(response))) apiService.getCollection(publicIdentifier, serviceMarketHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(response) } there was one(mockedServiceClient).get( path = s"/collections/$publicIdentifier", headers = createMarketHeaders(collectionsIdAuthToken), reads = Some(collectionReads), emptyResponse = false) } } "get collections" should { "return the status code and the response" in new ApiServiceScope { val response = CollectionsResponse(Seq(collection)) mockedServiceClient.get[CollectionsResponse](any, any, any, any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(response))) apiService.getCollections(serviceMarketHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(response) } there was one(mockedServiceClient).get( path = "/collections", headers = createMarketHeaders(collectionsAuthToken), reads = Some(collectionsResponseReads), emptyResponse = false) } } "categorize" should { "return the status code and the response" in new ApiServiceScope { mockedServiceClient.post[CategorizeRequest, CategorizeResponse](any, any, any, any, any)( any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(categorizeResponse))) apiService.categorize(categorizeRequest, serviceMarketHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(categorizeResponse) } there was one(mockedServiceClient).post( path = "/applications/categorize", headers = createMarketHeaders(categorizeAuthToken), body = categorizeRequest, reads = Some(categorizeResponseReads), emptyResponse = false)(categorizeRequestWrites) } } "categorizeDetail" should { "return the status code and the response" in new ApiServiceScope { mockedServiceClient .post[CategorizeRequest, CategorizeDetailResponse](any, any, any, any, any)(any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(categorizeDetailResponse))) apiService.categorizeDetail(categorizeRequest, serviceMarketHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(categorizeDetailResponse) } there was one(mockedServiceClient).post( path = "/applications/details", headers = createMarketHeaders(categorizeDetailAuthToken), body = categorizeRequest, reads = Some(categorizeDetailResponseReads), emptyResponse = false)(categorizeRequestWrites) } } "recommendations" should { "return the status code and the response and call with the right category" in new ApiServiceScope { val typePay = "FREE" mockedServiceClient.post[RecommendationsRequest, RecommendationsResponse]( any, any, any, any, any)(any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(recommendationsResponse))) apiService.recommendations( category, Option(typePay), recommendationsRequest, serviceMarketHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(recommendationsResponse) } there was one(mockedServiceClient).post( path = s"/recommendations/$category/$typePay", headers = createMarketHeaders(recommendationsAuthToken), body = recommendationsRequest, reads = Some(recommendationsResponseReads), emptyResponse = false)(recommendationsRequestWrites) } } "recommendations by apps" should { "return the status code and the response" in new ApiServiceScope { mockedServiceClient.post[RecommendationsByAppsRequest, RecommendationsByAppsResponse]( any, any, any, any, any)(any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(recommendationsByAppsResponse))) apiService .recommendationsByApps(recommendationsByAppsRequest, serviceMarketHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(recommendationsByAppsResponse) } there was one(mockedServiceClient).post( path = s"/recommendations", headers = createMarketHeaders(recommendationsByAppsAuthToken), body = recommendationsByAppsRequest, reads = Some(recommendationsByAppsResponseReads), emptyResponse = false)(recommendationsByAppsRequestWrites) } } "get subscriptions" should { "return the status code and the response" in new ApiServiceScope { val response = SubscriptionsResponse(subscriptions) mockedServiceClient.get[SubscriptionsResponse](any, any, any, any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(response))) apiService.getSubscriptions(serviceHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(response) } there was one(mockedServiceClient).get( path = "/collections/subscriptions", headers = createHeaders(subscriptionsAuthToken), reads = Some(subscriptionsResponseReads)) } } "subscribe" should { "return the status code" in new ApiServiceScope { mockedServiceClient.emptyPut[Unit](any, any, any, any) returns serviceRight(ServiceClientResponse(statusCodeOk, None)) apiService.subscribe(publicIdentifier, serviceHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beNone } there was one(mockedServiceClient).emptyPut( path = s"/collections/subscriptions/$publicIdentifier", headers = createHeaders(subscriptionsAuthToken), reads = None, emptyResponse = true) } } "unsubscribe" should { "return the status code" in new ApiServiceScope { mockedServiceClient.delete[Unit](any, any, any, any) returns serviceRight(ServiceClientResponse(statusCodeOk, None)) apiService.unsubscribe(publicIdentifier, serviceHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beNone } there was one(mockedServiceClient).delete( path = s"/collections/subscriptions/$publicIdentifier", headers = createHeaders(subscriptionsAuthToken), reads = None, emptyResponse = true) } } "updateViewShareCollection" should { "return the status code" in new ApiServiceScope { mockedServiceClient.emptyPost[Unit](any, any, any, any) returns serviceRight(ServiceClientResponse(statusCodeOk, None)) apiService.updateViewShareCollection(publicIdentifier, serviceHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beNone } there was one(mockedServiceClient).emptyPost( path = s"/collections/$publicIdentifier/views", headers = createHeaders(updateCollectionAuthToken), reads = None, emptyResponse = true) } } "rank apps" should { "return the status code and the response" in new ApiServiceScope { mockedServiceClient.post[RankAppsRequest, RankAppsResponse](any, any, any, any, any)(any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(rankAppsResponse))) apiService.rankApps(rankAppsRequest, serviceHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(rankAppsResponse) } there was one(mockedServiceClient).post( path = "/applications/rank", headers = createHeaders(rankAppsAuthToken), body = rankAppsRequest, reads = Some(rankAppsResponseReads), emptyResponse = false)(rankAppsRequestWrites) } } "search apps" should { "return the status code and the response" in new ApiServiceScope { mockedServiceClient.post[SearchRequest, SearchResponse](any, any, any, any, any)(any) returns serviceRight(ServiceClientResponse(statusCodeOk, Some(searchResponse))) apiService.search(searchRequest, serviceMarketHeader) mustRight { r => r.statusCode shouldEqual statusCodeOk r.data must beSome(searchResponse) } there was one(mockedServiceClient).post( path = "/applications/search", headers = createMarketHeaders(searchAuthToken), body = searchRequest, reads = Some(searchResponseReads), emptyResponse = false)(searchRequestWrites) } } } } ================================================ FILE: modules/api/src/test/scala/com/fortysevendeg/BaseTestSupport.scala ================================================ /* * Copyright 2017 47 Degrees, LLC. * * 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.fortysevendeg import org.specs2.execute.{AsResult, Result} import org.specs2.specification.{Around, Scope} trait BaseTestSupport extends Around with Scope { override def around[T: AsResult](t: => T): Result = AsResult.effectively(t) } ================================================ FILE: modules/app/build.sbt ================================================ platformTarget in Android := "android-24" ================================================ FILE: modules/app/crashlytics/templates/CrashlyticsManifest.xml ================================================ ================================================ FILE: modules/app/project/plugins.sbt ================================================ addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.1") ================================================ FILE: modules/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: modules/app/src/main/java/cards/nine/utils/SystemBarTintManager.java ================================================ /* * Copyright (C) 2013 readyState Software Ltd * * 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 cards.nine.utils; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.*; import android.widget.FrameLayout.LayoutParams; /** * Class to manage status and navigation bar tint effects when using KitKat * translucent system UI modes. * */ public class SystemBarTintManager { /** * The default system bar tint color value. */ public static final int DEFAULT_TINT_COLOR = Color.parseColor("#99000000"); private SystemBarConfig mConfig; private boolean mStatusBarAvailable; private boolean mNavBarAvailable; private boolean mStatusBarTintEnabled; private boolean mNavBarTintEnabled; private View mStatusBarTintView; private View mNavBarTintView; /** * Constructor. Call this in the host activity onCreate method after its * content view has been set. You should always create new instances when * the host activity is recreated. * * @param activity The host activity. */ @TargetApi(19) public SystemBarTintManager(Activity activity) { Window win = activity.getWindow(); ViewGroup decorViewGroup = (ViewGroup) win.getDecorView(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check theme attrs int[] attrs = {android.R.attr.windowTranslucentStatus, android.R.attr.windowTranslucentNavigation}; TypedArray a = activity.obtainStyledAttributes(attrs); mStatusBarAvailable = a.getBoolean(0, false); mNavBarAvailable = a.getBoolean(1, false); // check window flags WindowManager.LayoutParams winParams = win.getAttributes(); int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; if ((winParams.flags & bits) != 0) { mStatusBarAvailable = true; } bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; if ((winParams.flags & bits) != 0) { mNavBarAvailable = true; } } mConfig = new SystemBarConfig(activity, mStatusBarAvailable, mNavBarAvailable); // device might not have virtual navigation keys if (!mConfig.hasNavigationBar()) { mNavBarAvailable = false; } if (mStatusBarAvailable) { setupStatusBarView(activity, decorViewGroup); } if (mNavBarAvailable) { setupNavBarView(activity, decorViewGroup); } } /** * Enable tinting of the system status bar. * * If the platform is running Jelly Bean or earlier, or translucent system * UI modes have not been enabled in either the theme or via window flags, * then this method does nothing. * * @param enabled True to enable tinting, false to disable it (default). */ public void setStatusBarTintEnabled(boolean enabled) { mStatusBarTintEnabled = enabled; if (mStatusBarAvailable) { mStatusBarTintView.setVisibility(enabled ? View.VISIBLE : View.GONE); } } /** * Enable tinting of the system navigation bar. * * If the platform does not have soft navigation keys, is running Jelly Bean * or earlier, or translucent system UI modes have not been enabled in either * the theme or via window flags, then this method does nothing. * * @param enabled True to enable tinting, false to disable it (default). */ public void setNavigationBarTintEnabled(boolean enabled) { mNavBarTintEnabled = enabled; if (mNavBarAvailable) { mNavBarTintView.setVisibility(enabled ? View.VISIBLE : View.GONE); } } /** * Apply the specified color tint to all system UI bars. * * @param color The color of the background tint. */ public void setTintColor(int color) { setStatusBarTintColor(color); setNavigationBarTintColor(color); } /** * Apply the specified drawable or color resource to all system UI bars. * * @param res The identifier of the resource. */ public void setTintResource(int res) { setStatusBarTintResource(res); setNavigationBarTintResource(res); } /** * Apply the specified drawable to all system UI bars. * * @param drawable The drawable to use as the background, or null to remove it. */ public void setTintDrawable(Drawable drawable) { setStatusBarTintDrawable(drawable); setNavigationBarTintDrawable(drawable); } /** * Apply the specified color tint to the system status bar. * * @param color The color of the background tint. */ public void setStatusBarTintColor(int color) { if (mStatusBarAvailable) { mStatusBarTintView.setBackgroundColor(color); } } /** * Apply the specified drawable or color resource to the system status bar. * * @param res The identifier of the resource. */ public void setStatusBarTintResource(int res) { if (mStatusBarAvailable) { mStatusBarTintView.setBackgroundResource(res); } } /** * Apply the specified drawable to the system status bar. * * @param drawable The drawable to use as the background, or null to remove it. */ @SuppressWarnings("deprecation") public void setStatusBarTintDrawable(Drawable drawable) { if (mStatusBarAvailable) { mStatusBarTintView.setBackgroundDrawable(drawable); } } /** * Apply the specified color tint to the system navigation bar. * * @param color The color of the background tint. */ public void setNavigationBarTintColor(int color) { if (mNavBarAvailable) { mNavBarTintView.setBackgroundColor(color); } } /** * Apply the specified drawable or color resource to the system navigation bar. * * @param res The identifier of the resource. */ public void setNavigationBarTintResource(int res) { if (mNavBarAvailable) { mNavBarTintView.setBackgroundResource(res); } } /** * Apply the specified drawable to the system navigation bar. * * @param drawable The drawable to use as the background, or null to remove it. */ @SuppressWarnings("deprecation") public void setNavigationBarTintDrawable(Drawable drawable) { if (mNavBarAvailable) { mNavBarTintView.setBackgroundDrawable(drawable); } } /** * Get the system bar configuration. * * @return The system bar configuration for the current device configuration. */ public SystemBarConfig getConfig() { return mConfig; } /** * Is tinting enabled for the system status bar? * * @return True if enabled, False otherwise. */ public boolean isStatusBarTintEnabled() { return mStatusBarTintEnabled; } /** * Is tinting enabled for the system navigation bar? * * @return True if enabled, False otherwise. */ public boolean isNavBarTintEnabled() { return mNavBarTintEnabled; } private void setupStatusBarView(Context context, ViewGroup decorViewGroup) { mStatusBarTintView = new View(context); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, mConfig.getStatusBarHeight()); params.gravity = Gravity.TOP; if (mNavBarAvailable && !mConfig.isNavigationAtBottom()) { params.rightMargin = mConfig.getNavigationBarWidth(); } mStatusBarTintView.setLayoutParams(params); mStatusBarTintView.setBackgroundColor(DEFAULT_TINT_COLOR); mStatusBarTintView.setVisibility(View.GONE); decorViewGroup.addView(mStatusBarTintView); } private void setupNavBarView(Context context, ViewGroup decorViewGroup) { mNavBarTintView = new View(context); LayoutParams params; if (mConfig.isNavigationAtBottom()) { params = new LayoutParams(LayoutParams.MATCH_PARENT, mConfig.getNavigationBarHeight()); params.gravity = Gravity.BOTTOM; } else { params = new LayoutParams(mConfig.getNavigationBarWidth(), LayoutParams.MATCH_PARENT); params.gravity = Gravity.RIGHT; } mNavBarTintView.setLayoutParams(params); mNavBarTintView.setBackgroundColor(DEFAULT_TINT_COLOR); mNavBarTintView.setVisibility(View.GONE); decorViewGroup.addView(mNavBarTintView); } /** * Class which describes system bar sizing and other characteristics for the current * device configuration. * */ public class SystemBarConfig { private static final String STATUS_BAR_HEIGHT_RES_NAME = "status_bar_height"; private static final String NAV_BAR_HEIGHT_RES_NAME = "navigation_bar_height"; private static final String NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME = "navigation_bar_height_landscape"; private static final String NAV_BAR_WIDTH_RES_NAME = "navigation_bar_width"; private boolean mTranslucentStatusBar; private boolean mTranslucentNavBar; private int mStatusBarHeight; private int mActionBarHeight; private boolean mHasNavigationBar; private int mNavigationBarHeight; private int mNavigationBarWidth; private boolean mInPortrait; private float mSmallestWidthDp; private SystemBarConfig(Activity activity, boolean translucentStatusBar, boolean traslucentNavBar) { Resources res = activity.getResources(); mInPortrait = (res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT); mSmallestWidthDp = getSmallestWidthDp(activity); mStatusBarHeight = getInternalDimensionSize(res, STATUS_BAR_HEIGHT_RES_NAME); mActionBarHeight = getActionBarHeight(activity); mNavigationBarHeight = getNavigationBarHeight(activity); mNavigationBarWidth = getNavigationBarWidth(activity); mHasNavigationBar = (mNavigationBarHeight > 0); mTranslucentStatusBar = translucentStatusBar; mTranslucentNavBar = traslucentNavBar; } @TargetApi(14) private int getActionBarHeight(Context context) { int result = 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { TypedValue tv = new TypedValue(); context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true); result = context.getResources().getDimensionPixelSize(tv.resourceId); } return result; } @TargetApi(14) private int getNavigationBarHeight(Context context) { Resources res = context.getResources(); int result = 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { if (!ViewConfiguration.get(context).hasPermanentMenuKey()) { String key; if (mInPortrait) { key = NAV_BAR_HEIGHT_RES_NAME; } else { key = NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME; } return getInternalDimensionSize(res, key); } } return result; } @TargetApi(14) private int getNavigationBarWidth(Context context) { Resources res = context.getResources(); int result = 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { if (!ViewConfiguration.get(context).hasPermanentMenuKey()) { return getInternalDimensionSize(res, NAV_BAR_WIDTH_RES_NAME); } } return result; } private int getInternalDimensionSize(Resources res, String key) { int result = 0; int resourceId = res.getIdentifier(key, "dimen", "android"); if (resourceId > 0) { result = res.getDimensionPixelSize(resourceId); } return result; } @SuppressLint("NewApi") private float getSmallestWidthDp(Activity activity) { DisplayMetrics metrics = new DisplayMetrics(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { activity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics); } else { // TODO this is not correct, but we don't really care pre-kitkat activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); } float widthDp = metrics.widthPixels / metrics.density; float heightDp = metrics.heightPixels / metrics.density; return Math.min(widthDp, heightDp); } /** * Should a navigation bar appear at the bottom of the screen in the current * device configuration? A navigation bar may appear on the right side of * the screen in certain configurations. * * @return True if navigation should appear at the bottom of the screen, False otherwise. */ public boolean isNavigationAtBottom() { return (mSmallestWidthDp > 600 || mInPortrait); } /** * Get the height of the system status bar. * * @return The height of the status bar (in pixels). */ public int getStatusBarHeight() { return mStatusBarHeight; } /** * Get the height of the action bar. * * @return The height of the action bar (in pixels). */ public int getActionBarHeight() { return mActionBarHeight; } /** * Does this device have a system navigation bar? * * @return True if this device uses soft key navigation, False otherwise. */ public boolean hasNavigationBar() { return mHasNavigationBar; } /** * Get the height of the system navigation bar. * * @return The height of the navigation bar (in pixels). If the device does not have * soft navigation keys, this will always return 0. */ public int getNavigationBarHeight() { return mNavigationBarHeight; } /** * Get the width of the system navigation bar when it is placed vertically on the screen. * * @return The width of the navigation bar (in pixels). If the device does not have * soft navigation keys, this will always return 0. */ public int getNavigationBarWidth() { return mNavigationBarWidth; } /** * Get the layout inset for any system UI that appears at the top of the screen. * * @param withActionBar True to include the height of the action bar, False otherwise. * @return The layout inset (in pixels). */ public int getPixelInsetTop(boolean withActionBar) { return (mTranslucentStatusBar ? mStatusBarHeight : 0) + (withActionBar ? mActionBarHeight : 0); } /** * Get the layout inset for any system UI that appears at the bottom of the screen. * * @return The layout inset (in pixels). */ public int getPixelInsetBottom() { if (mTranslucentNavBar && isNavigationAtBottom()) { return mNavigationBarHeight; } else { return 0; } } /** * Get the layout inset for any system UI that appears at the right of the screen. * * @return The layout inset (in pixels). */ public int getPixelInsetRight() { if (mTranslucentNavBar && !isNavigationAtBottom()) { return mNavigationBarWidth; } else { return 0; } } } } ================================================ FILE: modules/app/src/main/res/anim/elevation_transition.xml ================================================ ================================================ FILE: modules/app/src/main/res/anim/grid_cards_layout_animation.xml ================================================ ================================================ FILE: modules/app/src/main/res/anim/list_slide_in_bottom_animation.xml ================================================ ================================================ FILE: modules/app/src/main/res/anim/slide_in_bottom.xml ================================================ ================================================ FILE: modules/app/src/main/res/color/wizard_text_button.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/background_icon_collection_detail.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/background_title_fab_menu_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/background_title_fab_menu_item_default.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/background_title_fab_menu_item_pressed.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/drawer_pager.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/drawer_pager_current.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/drawer_pager_default.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/fastscroller_bar.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/fastscroller_signal.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/mark_widget_resizing.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/publish_collection_wizard_pager.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/publish_collection_wizard_pager_current.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/stroke_widget_selected.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/wizard_inline_pager.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/wizard_inline_pager_current.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/wizard_inline_pager_default.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/wizard_pager.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/wizard_pager_current.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/wizard_pager_default.xml ================================================ ================================================ FILE: modules/app/src/main/res/drawable/workspaces_pager.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/about_header_preference.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/about_team_preference.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/add_moment_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/app_drawer_layout.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/app_drawer_panel.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/app_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/app_link_dialog_activity.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/app_select_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/apps_moment_layout.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/base_action_fragment.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/card_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/collection_bar_view_panel.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/collection_checkbox.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/collection_detail_fragment.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/collection_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/collections_actions_view_panel.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/collections_detail_activity.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/collections_detail_tab.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/collections_workspace_layout.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/color_info_item_dialog.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/contact_info_email_dialog.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/contact_info_general_dialog.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/contact_info_header.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/contact_info_phone_dialog.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/contact_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/dialog_edit_card.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/dialog_edit_text.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/edit_moment.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/edit_moment_hour_layout.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/edit_moment_wifi_layout.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/empty_profile_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/fab_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/fastscroller.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/header_list_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/icon_info_item_dialog.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/last_call_item.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/launcher_activity.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/list_action_apps_fragment.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/list_action_fragment.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/list_action_with_scroller_fragment.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/list_item_popup_menu.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/menu_header.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/moment_bar_view_panel.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/new_collection.xml ================================================ ================================================ FILE: modules/app/src/main/res/layout/private_collections_item.xml ================================================