Repository: alexstyl/Memento-Calendar
Branch: main
Commit: d224f0af53ee
Files: 903
Total size: 1.7 MB
Directory structure:
gitextract_87q2jt04/
├── .circleci/
│ ├── ci-scripts/
│ │ ├── accept-android-licenses.sh
│ │ ├── ensure-sdkmanager.sh
│ │ └── mock-google-services.json
│ └── config.yml
├── .github/
│ ├── CONTRIBUTING.md
│ ├── ISSUE_TEMPLATE.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── LICENSE
├── PEOPLE.md
├── README.md
├── android_common/
│ ├── build.gradle
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── alexstyl/
│ │ ├── android/
│ │ │ ├── AndroidLogger.kt
│ │ │ ├── Version.kt
│ │ │ ├── ViewVisibility.java
│ │ │ ├── preferences/
│ │ │ │ └── widget/
│ │ │ │ └── TimePreference.java
│ │ │ └── widget/
│ │ │ └── AppWidgetId.java
│ │ ├── resources/
│ │ │ ├── AndroidDimensionResources.java
│ │ │ └── DimensionResources.java
│ │ └── specialdates/
│ │ ├── AndroidStrings.kt
│ │ ├── TextViewLabelSetter.java
│ │ ├── date/
│ │ │ └── IntentDateExtensions.kt
│ │ ├── events/
│ │ │ └── database/
│ │ │ └── EventColumns.java
│ │ └── wear/
│ │ └── SharedConstants.java
│ └── res/
│ ├── values/
│ │ ├── colors.xml
│ │ ├── strings-non-translatable.xml
│ │ └── strings.xml
│ ├── values-cs/
│ │ └── strings.xml
│ ├── values-de/
│ │ └── strings.xml
│ ├── values-el/
│ │ └── strings.xml
│ ├── values-fr/
│ │ └── strings.xml
│ ├── values-it/
│ │ └── strings.xml
│ ├── values-lv/
│ │ └── bools.xml
│ ├── values-lv-rLV/
│ │ └── strings.xml
│ ├── values-nl/
│ │ └── strings.xml
│ └── values-sk/
│ └── bool.xml
├── android_mobile/
│ ├── .gitignore
│ ├── build.gradle
│ ├── google_services.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── debug/
│ │ ├── AndroidManifest.xml
│ │ ├── assets/
│ │ │ └── mock/
│ │ │ └── facebook-calendar.ics
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── alexstyl/
│ │ │ └── specialdates/
│ │ │ ├── DebugAppComponent.java
│ │ │ ├── DebugApplication.java
│ │ │ ├── OptionalDependencies.java
│ │ │ ├── debug/
│ │ │ │ ├── DebugActivity.java
│ │ │ │ ├── DebugFragment.kt
│ │ │ │ ├── DebugModule.java
│ │ │ │ └── DebugPreferences.java
│ │ │ ├── donate/
│ │ │ │ └── DebugDonationPreferences.java
│ │ │ └── events/
│ │ │ └── peopleevents/
│ │ │ └── DebugPeopleEventsUpdater.java
│ │ └── res/
│ │ └── layout/
│ │ ├── activity_debug.xml
│ │ ├── debug_activity_animations.xml
│ │ └── debug_activity_mixing_colors.xml
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── aidl/
│ │ │ └── com/
│ │ │ └── android/
│ │ │ └── vending/
│ │ │ └── billing/
│ │ │ └── IInAppBillingService.aidl
│ │ ├── java/
│ │ │ ├── android/
│ │ │ │ └── support/
│ │ │ │ └── v4/
│ │ │ │ └── preference/
│ │ │ │ ├── PreferenceFragment.java
│ │ │ │ └── PreferenceManagerCompat.java
│ │ │ └── com/
│ │ │ └── alexstyl/
│ │ │ ├── android/
│ │ │ │ ├── Bitmap.kt
│ │ │ │ ├── SimpleAnimatorListener.java
│ │ │ │ ├── Uri.kt
│ │ │ │ └── preferences/
│ │ │ │ └── PreferenceKeyId.java
│ │ │ ├── resources/
│ │ │ │ ├── AndroidColors.kt
│ │ │ │ └── ResourcesModule.java
│ │ │ └── specialdates/
│ │ │ ├── AndroidApplicationModule.java
│ │ │ ├── AppComponent.java
│ │ │ ├── DeviceConfigurationUpdatedReceiver.java
│ │ │ ├── EasyPreferences.java
│ │ │ ├── ExternalNavigator.java
│ │ │ ├── FabricTracker.java
│ │ │ ├── JobsCreator.kt
│ │ │ ├── MementoApplication.java
│ │ │ ├── MementoConstants.kt
│ │ │ ├── SQLArgumentBuilder.java
│ │ │ ├── ShareAppIntentCreator.java
│ │ │ ├── addevent/
│ │ │ │ ├── AccountData.java
│ │ │ │ ├── AddEventActivity.kt
│ │ │ │ ├── AddEventModule.kt
│ │ │ │ ├── AndroidAddEventView.kt
│ │ │ │ ├── AndroidContactOperationsExecutor.kt
│ │ │ │ ├── AndroidEventIcons.kt
│ │ │ │ ├── ContactDetailsListener.java
│ │ │ │ ├── ContactEventViewHolder.java
│ │ │ │ ├── ContactEventsAdapter.java
│ │ │ │ ├── ContactSuggestionViewHolder.java
│ │ │ │ ├── DiscardPromptDialog.java
│ │ │ │ ├── EventDatePickerDialogFragment.java
│ │ │ │ ├── ImageIntentFactory.java
│ │ │ │ ├── OnCameraClickedListener.java
│ │ │ │ ├── OperationsFactory.kt
│ │ │ │ ├── ToastDisplayer.kt
│ │ │ │ ├── ToolbarBackgroundAnimator.java
│ │ │ │ ├── ToolbarBackgroundFadingAnimator.java
│ │ │ │ ├── ToolbarBackgroundStubAnimator.java
│ │ │ │ ├── UriFilePathProvider.kt
│ │ │ │ ├── WriteableAccountsProvider.java
│ │ │ │ ├── bottomsheet/
│ │ │ │ │ ├── BottomSheetPicturesDialog.kt
│ │ │ │ │ ├── ClearImageViewHolder.java
│ │ │ │ │ ├── ImagePickerOptionViewHolder.kt
│ │ │ │ │ ├── ImagePickerOptionsAdapter.java
│ │ │ │ │ ├── IntentResolver.kt
│ │ │ │ │ ├── PhotoPickerViewModel.kt
│ │ │ │ │ └── PhotoPickerViewModelFactory.kt
│ │ │ │ └── ui/
│ │ │ │ ├── AvatarPickerView.java
│ │ │ │ ├── ContactSuggestionView.java
│ │ │ │ ├── ContactsAdapter.java
│ │ │ │ ├── DeviceContactsFilter.kt
│ │ │ │ └── EventDatePicker.java
│ │ │ ├── analytics/
│ │ │ │ ├── Action.java
│ │ │ │ ├── ActionWithParameters.java
│ │ │ │ ├── AnalyticsModule.kt
│ │ │ │ ├── CompositeAnalytics.kt
│ │ │ │ ├── FirebaseAnalyticsImpl.kt
│ │ │ │ └── MixPanel.kt
│ │ │ ├── contact/
│ │ │ │ ├── AndroidContactFactory.kt
│ │ │ │ ├── AndroidContactsProviderSource.kt
│ │ │ │ ├── AndroidContactsQuery.java
│ │ │ │ ├── ContactIntentExtractor.kt
│ │ │ │ ├── ContactsModule.kt
│ │ │ │ ├── EmptyContactSource.kt
│ │ │ │ └── FacebookContactsSource.kt
│ │ │ ├── dailyreminder/
│ │ │ │ ├── AlarmManagerCompat.java
│ │ │ │ ├── AndroidDailyReminderNotifier.kt
│ │ │ │ ├── AndroidDailyReminderScheduler.kt
│ │ │ │ ├── AndroidDailyReminderViewModelFactory.kt
│ │ │ │ ├── DailyReminderDebugPreferences.java
│ │ │ │ ├── DailyReminderJob.kt
│ │ │ │ ├── DailyReminderModule.java
│ │ │ │ ├── DailyReminderOreoChannelCreator.kt
│ │ │ │ ├── DailyReminderPreferences.kt
│ │ │ │ ├── NoActions.kt
│ │ │ │ ├── NotificationConstants.kt
│ │ │ │ ├── NotificationDailyReminderView.kt
│ │ │ │ └── actions/
│ │ │ │ ├── AndroidContactActionsView.kt
│ │ │ │ ├── ContactActionsModule.java
│ │ │ │ ├── ContactActionsPresenter.kt
│ │ │ │ ├── ContactActionsView.kt
│ │ │ │ └── PersonActionsActivity.kt
│ │ │ ├── date/
│ │ │ │ ├── AndroidDateLabelCreator.kt
│ │ │ │ └── DateModule.java
│ │ │ ├── donate/
│ │ │ │ ├── AndroidDonation.java
│ │ │ │ ├── AndroidDonationConstants.java
│ │ │ │ ├── AndroidDonationService.java
│ │ │ │ ├── DonateActivity.java
│ │ │ │ ├── DonateModule.java
│ │ │ │ ├── DonatePresenter.java
│ │ │ │ ├── DonationPreferences.java
│ │ │ │ ├── SimpleOnSeekBarChangeListener.java
│ │ │ │ └── util/
│ │ │ │ ├── IabBroadcastReceiver.java
│ │ │ │ ├── IabException.java
│ │ │ │ ├── IabHelper.java
│ │ │ │ ├── IabResult.java
│ │ │ │ ├── Inventory.java
│ │ │ │ ├── Purchase.java
│ │ │ │ ├── Security.java
│ │ │ │ └── SkuDetails.java
│ │ │ ├── events/
│ │ │ │ ├── ContactsObserver.java
│ │ │ │ ├── PreferenceChangedEventsUpdateTrigger.java
│ │ │ │ ├── bankholidays/
│ │ │ │ │ ├── BankHolidaysModule.java
│ │ │ │ │ └── BankHolidaysPreferences.java
│ │ │ │ ├── database/
│ │ │ │ │ ├── ContactColumns.java
│ │ │ │ │ ├── DatabaseContract.java
│ │ │ │ │ └── EventSQLiteOpenHelper.kt
│ │ │ │ ├── namedays/
│ │ │ │ │ ├── NamedayModule.java
│ │ │ │ │ ├── NamedayPreferences.java
│ │ │ │ │ ├── activity/
│ │ │ │ │ │ ├── AndroidNamedaysOnADayView.kt
│ │ │ │ │ │ ├── CelebratingContactViewHolder.kt
│ │ │ │ │ │ ├── NameViewHolder.kt
│ │ │ │ │ │ ├── NamedayScreenViewHolder.kt
│ │ │ │ │ │ ├── NamedaysInADayModule.java
│ │ │ │ │ │ ├── NamedaysOnADayActivity.kt
│ │ │ │ │ │ ├── NamedaysOnADayNavigator.kt
│ │ │ │ │ │ ├── NamedaysScreenAdapter.java
│ │ │ │ │ │ ├── NamedaysScreenViewHolderFactory.kt
│ │ │ │ │ │ └── NamedaysViewModelDiff.java
│ │ │ │ │ └── calendar/
│ │ │ │ │ └── resource/
│ │ │ │ │ └── AndroidJSONResourceLoader.kt
│ │ │ │ └── peopleevents/
│ │ │ │ ├── AndroidPeopleEventsPersister.kt
│ │ │ │ ├── AndroidPeopleEventsProvider.kt
│ │ │ │ ├── AndroidPeopleEventsRepository.kt
│ │ │ │ ├── AndroidUpcomingEventSettings.kt
│ │ │ │ ├── ContactEventsMarshaller.java
│ │ │ │ ├── CustomEventProvider.kt
│ │ │ │ └── PeopleEventsModule.kt
│ │ │ ├── facebook/
│ │ │ │ ├── AndroidFacebookPreferences.java
│ │ │ │ ├── FacebookLogoutService.java
│ │ │ │ ├── FacebookModule.java
│ │ │ │ ├── FacebookProfileActivity.java
│ │ │ │ ├── FacebookProfilePresenter.java
│ │ │ │ ├── FacebookProfileView.java
│ │ │ │ ├── OnFacebookLogOutCallback.java
│ │ │ │ ├── ScreenOrientationLock.java
│ │ │ │ ├── friendimport/
│ │ │ │ │ ├── CalendarURLCreator.java
│ │ │ │ │ ├── FacebookFriendsIntentService.java
│ │ │ │ │ ├── FacebookFriendsPersister.java
│ │ │ │ │ └── FacebookFriendsScheduler.java
│ │ │ │ └── login/
│ │ │ │ ├── CookieResetter.java
│ │ │ │ ├── CredentialsExtractor.java
│ │ │ │ ├── FBImportClient.java
│ │ │ │ ├── FacebookImportView.java
│ │ │ │ ├── FacebookLogInActivity.java
│ │ │ │ ├── FacebookLogInCallback.java
│ │ │ │ ├── FacebookLogInException.java
│ │ │ │ ├── FacebookWebView.java
│ │ │ │ └── UserCredentialsExtractorTask.java
│ │ │ ├── home/
│ │ │ │ ├── DonationBannerView.kt
│ │ │ │ ├── HomeActivity.java
│ │ │ │ ├── HomeNavigator.kt
│ │ │ │ ├── HomeViewPagerAdapter.java
│ │ │ │ ├── OnCloseBannerListener.kt
│ │ │ │ └── SearchTransitioner.java
│ │ │ ├── images/
│ │ │ │ ├── AndroidContactsImageDownloader.kt
│ │ │ │ ├── CrossFadeBitmapDisplayer.java
│ │ │ │ ├── CrossFadeCircleBitmapDisplayer.java
│ │ │ │ ├── DecodedImage.java
│ │ │ │ ├── ImageDecoder.kt
│ │ │ │ ├── ImageLoadedConsumer.java
│ │ │ │ ├── ImageLoader.kt
│ │ │ │ ├── ImageModule.java
│ │ │ │ ├── NutraBaseImageDecoder.java
│ │ │ │ ├── SimpleImageLoadedConsumer.java
│ │ │ │ └── UILImageLoader.java
│ │ │ ├── people/
│ │ │ │ ├── ImportFromFacebookViewHolder.kt
│ │ │ │ ├── NoContactViewHolder.kt
│ │ │ │ ├── PeopleAdapter.java
│ │ │ │ ├── PeopleDiffCallback.kt
│ │ │ │ ├── PeopleFragment.java
│ │ │ │ ├── PeopleItemDecorator.java
│ │ │ │ ├── PeopleModule.java
│ │ │ │ └── PeopleViewHolder.java
│ │ │ ├── permissions/
│ │ │ │ ├── AndroidPermissions.kt
│ │ │ │ └── ContactPermissionActivity.java
│ │ │ ├── person/
│ │ │ │ ├── AndroidContactActions.kt
│ │ │ │ ├── AndroidContactActionsProvider.kt
│ │ │ │ ├── AndroidPersonView.kt
│ │ │ │ ├── BottomSheetIntentAdapter.java
│ │ │ │ ├── BottomSheetIntentDialog.kt
│ │ │ │ ├── BottomSheetIntentListener.java
│ │ │ │ ├── CallMethod.kt
│ │ │ │ ├── CallViewHolder.java
│ │ │ │ ├── CompositeContactActionsProvider.kt
│ │ │ │ ├── ContactAction.kt
│ │ │ │ ├── ContactActionViewModel.kt
│ │ │ │ ├── ContactActionsAdapter.java
│ │ │ │ ├── ContactActionsPageViewHolder.kt
│ │ │ │ ├── ContactActionsProvider.kt
│ │ │ │ ├── ContactItemsAdapter.java
│ │ │ │ ├── EventAdapter.java
│ │ │ │ ├── EventPageViewHolder.kt
│ │ │ │ ├── EventPressedListener.java
│ │ │ │ ├── EventViewHolder.java
│ │ │ │ ├── FacebookContactActionsProvider.kt
│ │ │ │ ├── IntentOptionViewHolder.java
│ │ │ │ ├── IntentOptionViewModel.java
│ │ │ │ ├── PageViewHolder.kt
│ │ │ │ ├── PersonActivity.kt
│ │ │ │ ├── PersonAvailableActionsViewModel.kt
│ │ │ │ ├── PersonDetailsNavigator.kt
│ │ │ │ ├── PersonDetailsViewModelFactory.kt
│ │ │ │ ├── PersonInfoViewModel.kt
│ │ │ │ ├── PersonModule.kt
│ │ │ │ ├── PersonPresenter.kt
│ │ │ │ └── PersonView.kt
│ │ │ ├── receiver/
│ │ │ │ └── BootCompleteReceiver.java
│ │ │ ├── search/
│ │ │ │ ├── BackKeyEditText.java
│ │ │ │ ├── CaseInsensitiveComparator.java
│ │ │ │ ├── ContactEventViewModel.kt
│ │ │ │ ├── ContactEventViewModelFactory.kt
│ │ │ │ ├── DelayedTextWatcher.java
│ │ │ │ ├── MoreViewHolder.java
│ │ │ │ ├── NameSuggestionsAdapter.java
│ │ │ │ ├── NamedayCard.java
│ │ │ │ ├── NamedaysLoader.java
│ │ │ │ ├── NamesFilter.java
│ │ │ │ ├── NoResultsViewHolder.java
│ │ │ │ ├── OnBackKeyPressedListener.java
│ │ │ │ ├── SearchActivity.java
│ │ │ │ ├── SearchBar.java
│ │ │ │ ├── SearchHintCreator.java
│ │ │ │ ├── SearchLoader.java
│ │ │ │ ├── SearchModule.java
│ │ │ │ ├── SearchNavigator.kt
│ │ │ │ ├── SearchResultAdapter.java
│ │ │ │ ├── SearchResultContactViewHolder.java
│ │ │ │ ├── SearchResultNamedayViewHolder.java
│ │ │ │ ├── SearchResults.java
│ │ │ │ ├── SuggstedNameViewHolder.java
│ │ │ │ └── ToggleVisibilityOnFocus.java
│ │ │ ├── settings/
│ │ │ │ ├── ClickableRingtonePreference.java
│ │ │ │ ├── DailyReminderActivity.java
│ │ │ │ ├── DailyReminderFragment.kt
│ │ │ │ ├── DailyReminderNavigator.kt
│ │ │ │ ├── MementoThemeNameComparator.java
│ │ │ │ ├── NamedayListPreference.java
│ │ │ │ ├── OnlyGreekSupportedDialog.java
│ │ │ │ ├── PreferenceNotFoundException.kt
│ │ │ │ ├── ThemeSelectAdapter.java
│ │ │ │ ├── ThemeSelectDialog.java
│ │ │ │ ├── ThemeViewHolder.java
│ │ │ │ └── UserSettingsFragment.java
│ │ │ ├── support/
│ │ │ │ ├── AskForSupport.kt
│ │ │ │ ├── CallForRatingPreferences.java
│ │ │ │ ├── Emoticon.java
│ │ │ │ ├── OnSupportCardClickListener.java
│ │ │ │ └── RateDialog.java
│ │ │ ├── theming/
│ │ │ │ ├── AttributeExtractor.java
│ │ │ │ ├── DrawableTinter.java
│ │ │ │ ├── MementoTheme.kt
│ │ │ │ ├── ThemeMonitor.java
│ │ │ │ ├── Themer.kt
│ │ │ │ ├── ThemingModule.java
│ │ │ │ └── ThemingPreferences.kt
│ │ │ ├── transition/
│ │ │ │ ├── FadeInTransition.java
│ │ │ │ ├── FadeOutTransition.java
│ │ │ │ └── SimpleTransitionListener.java
│ │ │ ├── ui/
│ │ │ │ ├── DummyHideStatusBarListener.java
│ │ │ │ ├── HorizontalDivider.java
│ │ │ │ ├── LolipopHideStatusBarListener.java
│ │ │ │ ├── MementoCardView.java
│ │ │ │ ├── ViewFader.java
│ │ │ │ ├── base/
│ │ │ │ │ ├── MementoActivity.kt
│ │ │ │ │ ├── MementoDialog.java
│ │ │ │ │ ├── MementoFragment.java
│ │ │ │ │ ├── MementoPreferenceActivity.kt
│ │ │ │ │ ├── MementoPreferenceFragment.kt
│ │ │ │ │ └── ThemedMementoActivity.kt
│ │ │ │ ├── dialog/
│ │ │ │ │ └── ProgressFragmentDialog.java
│ │ │ │ ├── loader/
│ │ │ │ │ └── SimpleAsyncTaskLoader.java
│ │ │ │ └── widget/
│ │ │ │ ├── AndroidLetterPainter.java
│ │ │ │ ├── AvatarLayout.java
│ │ │ │ ├── ColorImageView.java
│ │ │ │ ├── ForegroundLinearLayout.java
│ │ │ │ ├── LogoView.java
│ │ │ │ ├── MementoToolbar.java
│ │ │ │ ├── SpacesItemDecoration.java
│ │ │ │ └── ViewModule.java
│ │ │ ├── upcoming/
│ │ │ │ ├── AndroidUpcomingDateStringCreator.kt
│ │ │ │ ├── AndroidUpcomingMVPView.kt
│ │ │ │ ├── BankholidayViewHolder.kt
│ │ │ │ ├── ContactEventViewHolder.kt
│ │ │ │ ├── DateHeaderViewHolder.kt
│ │ │ │ ├── DatePickerDialogFragment.java
│ │ │ │ ├── NamedaysViewHolder.kt
│ │ │ │ ├── PeopleEventsRefreshJob.kt
│ │ │ │ ├── UpcomingEventsAdapter.java
│ │ │ │ ├── UpcomingEventsDecorator.java
│ │ │ │ ├── UpcomingEventsDiffCallback.kt
│ │ │ │ ├── UpcomingEventsFragment.kt
│ │ │ │ ├── UpcomingEventsModule.kt
│ │ │ │ ├── UpcomingRowViewHolder.kt
│ │ │ │ ├── UpcomingViewHolderFactory.kt
│ │ │ │ ├── view/
│ │ │ │ │ ├── ExposedSearchToolbar.java
│ │ │ │ │ └── OnUpcomingEventClickedListener.java
│ │ │ │ └── widget/
│ │ │ │ ├── RecentUpcomingPeopleEventsModule.kt
│ │ │ │ ├── list/
│ │ │ │ │ ├── BankHolidayBinder.kt
│ │ │ │ │ ├── CircularAvatarFactory.java
│ │ │ │ │ ├── ContactEventBinder.kt
│ │ │ │ │ ├── DateHeaderBinder.kt
│ │ │ │ │ ├── NamedaysBinder.kt
│ │ │ │ │ ├── UpcomingEventViewBinder.java
│ │ │ │ │ ├── UpcomingEventsRemoteViewService.java
│ │ │ │ │ ├── UpcomingEventsScrollingAppWidgetProvider.java
│ │ │ │ │ ├── UpcomingEventsScrollingWidgetView.java
│ │ │ │ │ ├── UpcomingEventsViewsFactory.java
│ │ │ │ │ └── WidgetRouterActivity.kt
│ │ │ │ └── today/
│ │ │ │ ├── AndroidRecentPeopleEventsView.kt
│ │ │ │ ├── LuminanceAnalyzer.kt
│ │ │ │ ├── SimpleOnSeekBarChangeListener.java
│ │ │ │ ├── TodayAppWidgetProvider.kt
│ │ │ │ ├── TodayUpcomingEventsView.java
│ │ │ │ ├── TransparencyColorCalculator.java
│ │ │ │ ├── UpcomingWidgetConfigurationPanel.java
│ │ │ │ ├── UpcomingWidgetConfigureActivity.kt
│ │ │ │ ├── UpcomingWidgetPreferences.kt
│ │ │ │ ├── UpcomingWidgetPreviewLayout.java
│ │ │ │ ├── UserOptions.kt
│ │ │ │ ├── WidgetColorCalculator.java
│ │ │ │ ├── WidgetImageLoader.java
│ │ │ │ └── WidgetVariant.java
│ │ │ ├── util/
│ │ │ │ ├── GreekNameUtils.java
│ │ │ │ └── NaturalLanguageUtils.java
│ │ │ └── wear/
│ │ │ ├── WearSyncService.java
│ │ │ └── WearSyncUpcomingEventsView.java
│ │ └── res/
│ │ ├── anim/
│ │ │ ├── bounce.xml
│ │ │ ├── grow_from_bottom.xml
│ │ │ ├── grow_from_top.xml
│ │ │ ├── heartbeat.xml
│ │ │ ├── slide_in_below.xml
│ │ │ ├── slide_in_from_above.xml
│ │ │ ├── slide_in_from_below.xml
│ │ │ ├── slide_out_above.xml
│ │ │ ├── slide_out_from_above.xml
│ │ │ ├── slide_out_from_below.xml
│ │ │ ├── slide_up_left.xml
│ │ │ ├── slide_up_right.xml
│ │ │ └── stay.xml
│ │ ├── drawable/
│ │ │ ├── ab_background_textured_dayslight.xml
│ │ │ ├── background_daymarker.xml
│ │ │ ├── background_suggestions.xml
│ │ │ ├── black_to_transparent_gradient_facing_down.xml
│ │ │ ├── btn_cab_done_dayslight.xml
│ │ │ ├── card_noshadow.xml
│ │ │ ├── dayslight_progress_horizontal_holo_light.xml
│ │ │ ├── dayslight_progress_indeterminate_horizontal_holo_light.xml
│ │ │ ├── divider_top_horizontal.xml
│ │ │ ├── ic_add_contact_42px.xml
│ │ │ ├── ic_add_person_24px.xml
│ │ │ ├── ic_arrow_back_white_24dp.xml
│ │ │ ├── ic_bankholidays.xml
│ │ │ ├── ic_bankholidays_disabled.xml
│ │ │ ├── ic_call.xml
│ │ │ ├── ic_camera.xml
│ │ │ ├── ic_check_white.xml
│ │ │ ├── ic_clear.xml
│ │ │ ├── ic_close_black.xml
│ │ │ ├── ic_close_white.xml
│ │ │ ├── ic_contacts.xml
│ │ │ ├── ic_contacts_disabled.xml
│ │ │ ├── ic_donate.xml
│ │ │ ├── ic_events.xml
│ │ │ ├── ic_f_icon.xml
│ │ │ ├── ic_facebook_import_friends.xml
│ │ │ ├── ic_facebook_like.xml
│ │ │ ├── ic_facebook_logo_traced.xml
│ │ │ ├── ic_facebook_sad.xml
│ │ │ ├── ic_friend_invite.xml
│ │ │ ├── ic_gift.xml
│ │ │ ├── ic_github.xml
│ │ │ ├── ic_licenses.xml
│ │ │ ├── ic_menu.xml
│ │ │ ├── ic_message.xml
│ │ │ ├── ic_namedays.xml
│ │ │ ├── ic_namedays_disabled.xml
│ │ │ ├── ic_person_120.xml
│ │ │ ├── ic_person_24dp.xml
│ │ │ ├── ic_person_96dp.xml
│ │ │ ├── ic_person_light_24dp.xml
│ │ │ ├── ic_search_black_24dp.xml
│ │ │ ├── ic_settings.xml
│ │ │ ├── progress_horizontal_dayslight.xml
│ │ │ ├── selectable_background_dayslight.xml
│ │ │ ├── spinner_background_ab_dayslight.xml
│ │ │ └── tab_indicator_ab_dayslight.xml
│ │ ├── drawable-v21/
│ │ │ ├── blue_ripple.xml
│ │ │ ├── neutral_ripple.xml
│ │ │ ├── neutral_ripple_no_mask.xml
│ │ │ ├── red_ripple.xml
│ │ │ └── red_ripple_no_mask.xml
│ │ ├── layout/
│ │ │ ├── abc_dropdown_title.xml
│ │ │ ├── activity_add_event.xml
│ │ │ ├── activity_call.xml
│ │ │ ├── activity_contact_permission_request.xml
│ │ │ ├── activity_dailyreminder.xml
│ │ │ ├── activity_debug_facebook.xml
│ │ │ ├── activity_donate.xml
│ │ │ ├── activity_facebook_log_in.xml
│ │ │ ├── activity_facebook_profile.xml
│ │ │ ├── activity_home.xml
│ │ │ ├── activity_namedays.xml
│ │ │ ├── activity_person.xml
│ │ │ ├── activity_preferences.xml
│ │ │ ├── activity_rate_dialog.xml
│ │ │ ├── activity_search.xml
│ │ │ ├── activity_try.xml
│ │ │ ├── activity_upcoming_events_widget_configure__first_frame.xml
│ │ │ ├── activity_upcoming_events_widget_configure__start.xml
│ │ │ ├── card_bankholiday.xml
│ │ │ ├── card_compact_support_heart.xml
│ │ │ ├── card_contact_event_full.xml
│ │ │ ├── card_full_support_heart.xml
│ │ │ ├── card_load_more.xml
│ │ │ ├── card_nameday_single.xml
│ │ │ ├── card_namedays.xml
│ │ │ ├── dialog_birthday_picker.xml
│ │ │ ├── dialog_bottom_dialog.xml
│ │ │ ├── dialog_donate.xml
│ │ │ ├── dialog_pick_image.xml
│ │ │ ├── dialog_progress.xml
│ │ │ ├── dialog_rate_prompt.xml
│ │ │ ├── dialog_support.xml
│ │ │ ├── dialog_translate.xml
│ │ │ ├── fragment_first.xml
│ │ │ ├── fragment_people.xml
│ │ │ ├── fragment_upcoming_events.xml
│ │ │ ├── merge_avatar_picker_view.xml
│ │ │ ├── merge_birthday_picker.xml
│ │ │ ├── merge_birthdaypicker_label_view.xml
│ │ │ ├── merge_color_imageview.xml
│ │ │ ├── merge_compact_cardview.xml
│ │ │ ├── merge_contact_permission_required.xml
│ │ │ ├── merge_contact_suggestion_view.xml
│ │ │ ├── merge_daymarker.xml
│ │ │ ├── merge_donation_banner_view.xml
│ │ │ ├── merge_dummy_view.xml
│ │ │ ├── merge_logo_view.xml
│ │ │ ├── merge_memento_toolbar.xml
│ │ │ ├── merge_namedaycardview.xml
│ │ │ ├── merge_no_contact_events.xml
│ │ │ ├── merge_searchbar.xml
│ │ │ ├── merge_upcoming_widget_configure_panel.xml
│ │ │ ├── merge_upcoming_widget_preview.xml
│ │ │ ├── nameday_date.xml
│ │ │ ├── names_suggestions.xml
│ │ │ ├── navigation_header.xml
│ │ │ ├── navigation_new_feature.xml
│ │ │ ├── page_person_items.xml
│ │ │ ├── preference_list_fragment.xml
│ │ │ ├── preference_theme_select.xml
│ │ │ ├── row_account.xml
│ │ │ ├── row_add_event_contact_event.xml
│ │ │ ├── row_add_event_contact_suggestion.xml
│ │ │ ├── row_contact.xml
│ │ │ ├── row_datatype.xml
│ │ │ ├── row_image_option.xml
│ │ │ ├── row_nameday_checkbox.xml
│ │ │ ├── row_nameday_contact.xml
│ │ │ ├── row_nameday_name.xml
│ │ │ ├── row_no_search_results.xml
│ │ │ ├── row_people.xml
│ │ │ ├── row_people_import_from_facebook.xml
│ │ │ ├── row_people_no_contacts.xml
│ │ │ ├── row_person_action.xml
│ │ │ ├── row_person_event.xml
│ │ │ ├── row_search_result_contact_event.xml
│ │ │ ├── row_suggested_name.xml
│ │ │ ├── row_theme_select.xml
│ │ │ ├── row_upcoming_events_bankholiday.xml
│ │ │ ├── row_upcoming_events_contact_event.xml
│ │ │ ├── row_upcoming_events_date_header.xml
│ │ │ ├── row_upcoming_events_nameday.xml
│ │ │ ├── toolbar_search.xml
│ │ │ ├── widget_loading.xml
│ │ │ ├── widget_prompt_permissions.xml
│ │ │ ├── widget_today.xml
│ │ │ ├── widget_today_nocontacts.xml
│ │ │ ├── widget_upcoming_events.xml
│ │ │ ├── widget_upcoming_events_list_contact_event.xml
│ │ │ ├── widget_upcoming_events_list_date.xml
│ │ │ ├── widget_upcoming_events_list_nameday.xml
│ │ │ └── widget_upcomingevents_list_bankholiday.xml
│ │ ├── layout-land/
│ │ │ └── activity_add_event.xml
│ │ ├── menu/
│ │ │ ├── menu_date_details.xml
│ │ │ ├── menu_nameday_view.xml
│ │ │ ├── menu_nav_drawer.xml
│ │ │ ├── menu_person_details.xml
│ │ │ └── menu_search.xml
│ │ ├── raw/
│ │ │ ├── cs_namedays.json
│ │ │ ├── gr_namedays.json
│ │ │ ├── hu_namedays.json
│ │ │ ├── it_namedays.json
│ │ │ ├── lv_ext_namedays.json
│ │ │ ├── lv_namedays.json
│ │ │ ├── ro_namedays.json
│ │ │ ├── ru_namedays.json
│ │ │ └── sk_namedays.json
│ │ ├── transition/
│ │ │ ├── changebounds.xml
│ │ │ ├── explode.xml
│ │ │ └── fade_and_changebounds.xml
│ │ ├── values/
│ │ │ ├── add_event-resources.xml
│ │ │ ├── admob.xml
│ │ │ ├── animations_dimens.xml
│ │ │ ├── arrays.xml
│ │ │ ├── attrs.xml
│ │ │ ├── available_themes.xml
│ │ │ ├── bankholidays_preferences-resources.xml
│ │ │ ├── bithday_picker-styles.xml
│ │ │ ├── bools.xml
│ │ │ ├── bottom_sheet-resources.xml
│ │ │ ├── card-resources.xml
│ │ │ ├── colors.xml
│ │ │ ├── colors_dayslight.xml
│ │ │ ├── colors_support.xml
│ │ │ ├── contact_cards-resources.xml
│ │ │ ├── contact_permission-resources.xml
│ │ │ ├── date_details-resources.xml
│ │ │ ├── debug_keys.xml
│ │ │ ├── dimens-contact_details.xml
│ │ │ ├── dimens.xml
│ │ │ ├── divider-res.xml
│ │ │ ├── donate-resources.xml
│ │ │ ├── drawable.xml
│ │ │ ├── facebook-resources.xml
│ │ │ ├── ids.xml
│ │ │ ├── integers.xml
│ │ │ ├── main_preferences-resources.xml
│ │ │ ├── namedayscreen-resources.xml
│ │ │ ├── no_contacts.xml
│ │ │ ├── nocontacts_styles.xml
│ │ │ ├── people_resources.xml
│ │ │ ├── person-resources.xml
│ │ │ ├── pref_fragment_dimens.xml
│ │ │ ├── pref_fragment_strings.xml
│ │ │ ├── search-resources.xml
│ │ │ ├── string_keys.xml
│ │ │ ├── styles.xml
│ │ │ ├── support-resources.xml
│ │ │ ├── support-strings.xml
│ │ │ ├── themes-strings.xml
│ │ │ ├── themes.xml
│ │ │ ├── today_widget_strings.xml
│ │ │ ├── toolbar-resources.xml
│ │ │ ├── upcoming-resources.xml
│ │ │ ├── upcoming_widget_dimens.xml
│ │ │ ├── upcoming_widget_preview-resources.xml
│ │ │ └── widget_upcoming_events-resources.xml
│ │ ├── values-el/
│ │ │ ├── bool.xml
│ │ │ └── support-strings.xml
│ │ ├── values-it/
│ │ │ └── bool.xml
│ │ ├── values-land/
│ │ │ ├── bools.xml
│ │ │ ├── dimens.xml
│ │ │ ├── integers.xml
│ │ │ └── toolbar-resources.xml
│ │ ├── values-land-v23/
│ │ │ └── themes.xml
│ │ ├── values-large/
│ │ │ └── bool.xml
│ │ ├── values-sw600dp/
│ │ │ ├── bottom_sheet-resources.xml
│ │ │ ├── dimens.xml
│ │ │ ├── integers.xml
│ │ │ └── toolbar-resources.xml
│ │ ├── values-sw720dp/
│ │ │ └── bottom_sheet-resources.xml
│ │ ├── values-sw720dp-land/
│ │ │ └── dimens.xml
│ │ ├── values-v21/
│ │ │ ├── dimens.xml
│ │ │ ├── drawable.xml
│ │ │ └── styles.xml
│ │ ├── xml/
│ │ │ ├── file_paths.xml
│ │ │ ├── preference_dailyreminder.xml
│ │ │ ├── preference_debug.xml
│ │ │ ├── preference_main.xml
│ │ │ ├── widget_info_upcoming_events_list.xml
│ │ │ └── widget_info_upcoming_events_simple.xml
│ │ └── xml-v26/
│ │ └── preference_dailyreminder.xml
│ └── test/
│ └── java/
│ └── com/
│ └── alexstyl/
│ ├── resources/
│ │ └── JavaStrings.kt
│ └── specialdates/
│ └── person/
│ ├── CompositeContactActionsProviderTest.kt
│ └── PersonInfoViewModelFactoryTest.kt
├── android_wear/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── alexstyl/
│ │ └── specialdates/
│ │ ├── ContactEventsActivity.java
│ │ ├── ContactEventsProviderService.java
│ │ ├── DataChangedListenerService.java
│ │ └── WearCommunicationService.java
│ └── res/
│ ├── layout/
│ │ └── activity_contact_events.xml
│ └── values/
│ ├── contact_events-resource.xml
│ └── strings.xml
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── memento/
│ ├── .gitignore
│ ├── build.gradle
│ ├── config.yml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── android/
│ │ │ └── LruCache.java
│ │ └── com/
│ │ └── alexstyl/
│ │ ├── Logger.kt
│ │ ├── gsc/
│ │ │ ├── Index.java
│ │ │ ├── Sound.kt
│ │ │ ├── SoundComparer.kt
│ │ │ └── SoundRules.kt
│ │ ├── resources/
│ │ │ └── Colors.kt
│ │ └── specialdates/
│ │ ├── CrashAndErrorTracker.kt
│ │ ├── EventsUpdateTrigger.java
│ │ ├── LabelSetter.java
│ │ ├── Optional.kt
│ │ ├── SoundWordComparator.java
│ │ ├── Strings.kt
│ │ ├── TimeOfDay.kt
│ │ ├── UpcomingEventsView.kt
│ │ ├── WordComparator.java
│ │ ├── addevent/
│ │ │ ├── AddEventContactEventViewModel.kt
│ │ │ ├── AddEventView.kt
│ │ │ ├── AddEventViewModelFactory.kt
│ │ │ ├── AddEventsPresenter.kt
│ │ │ ├── ContactOperations.kt
│ │ │ ├── ContactOperationsExecutor.kt
│ │ │ ├── ContactsSearch.kt
│ │ │ ├── EventIcons.kt
│ │ │ ├── MessageDisplayer.kt
│ │ │ └── operations/
│ │ │ ├── ContactOperation.kt
│ │ │ ├── InsertContact.kt
│ │ │ ├── InsertEvent.kt
│ │ │ ├── InsertImage.kt
│ │ │ └── UpdateContact.kt
│ │ ├── analytics/
│ │ │ ├── Analytics.kt
│ │ │ ├── Screen.kt
│ │ │ └── Widget.kt
│ │ ├── contact/
│ │ │ ├── Contact.kt
│ │ │ ├── ContactCache.kt
│ │ │ ├── ContactNotFoundException.kt
│ │ │ ├── ContactSource.java
│ │ │ ├── Contacts.kt
│ │ │ ├── ContactsProvider.kt
│ │ │ ├── ContactsProviderSource.kt
│ │ │ ├── DisplayName.kt
│ │ │ └── Names.kt
│ │ ├── dailyreminder/
│ │ │ ├── BankHolidayNotificationViewModel.kt
│ │ │ ├── ContactActionViewModel.kt
│ │ │ ├── ContactEventNotificationViewModel.kt
│ │ │ ├── DailyReminderNotifier.kt
│ │ │ ├── DailyReminderPresenter.kt
│ │ │ ├── DailyReminderScheduler.kt
│ │ │ ├── DailyReminderUserSettings.kt
│ │ │ ├── DailyReminderView.kt
│ │ │ ├── DailyReminderViewModel.kt
│ │ │ ├── DailyReminderViewModelFactory.kt
│ │ │ ├── NamedaysNotificationViewModel.kt
│ │ │ ├── NotificationViewModel.kt
│ │ │ └── SummaryNotificationViewModel.kt
│ │ ├── date/
│ │ │ ├── ContactEvent.kt
│ │ │ ├── Date.kt
│ │ │ ├── DateAndTime.kt
│ │ │ ├── DateComparator.java
│ │ │ ├── DateLabelCreator.kt
│ │ │ ├── DateParseException.kt
│ │ │ ├── DateParser.kt
│ │ │ ├── Dates.kt
│ │ │ ├── MonthInt.java
│ │ │ ├── Months.java
│ │ │ └── TimePeriod.kt
│ │ ├── donate/
│ │ │ ├── DonateMonitor.kt
│ │ │ ├── Donation.java
│ │ │ ├── DonationCallbacks.java
│ │ │ └── DonationService.java
│ │ ├── events/
│ │ │ ├── Event.kt
│ │ │ ├── SettingsPresenter.kt
│ │ │ ├── bankholidays/
│ │ │ │ ├── BankHoliday.kt
│ │ │ │ ├── BankHolidayProvider.kt
│ │ │ │ ├── BankHolidaysUserSettings.kt
│ │ │ │ └── GreekBankHolidaysCalculator.java
│ │ │ ├── database/
│ │ │ │ └── EventTypeId.java
│ │ │ ├── namedays/
│ │ │ │ ├── NameCelebrations.kt
│ │ │ │ ├── NamedayBundle.java
│ │ │ │ ├── NamedayDatabaseRefresher.kt
│ │ │ │ ├── NamedayLocale.java
│ │ │ │ ├── NamedayUserSettings.java
│ │ │ │ ├── NamedaysList.java
│ │ │ │ ├── NamesInADate.kt
│ │ │ │ ├── activity/
│ │ │ │ │ ├── CelebratingContactViewModel.kt
│ │ │ │ │ ├── NamedayScreenViewModel.kt
│ │ │ │ │ ├── NamedayScreenViewType.java
│ │ │ │ │ ├── NamedaysInADayPresenter.kt
│ │ │ │ │ ├── NamedaysOnADayView.kt
│ │ │ │ │ ├── NamedaysViewModel.kt
│ │ │ │ │ └── NamedaysViewModelFactory.kt
│ │ │ │ └── calendar/
│ │ │ │ ├── EasternNameday.java
│ │ │ │ ├── EasternNamedaysExtractor.java
│ │ │ │ ├── NamedayCalendar.java
│ │ │ │ ├── OrthodoxEasterCalculator.java
│ │ │ │ └── resource/
│ │ │ │ ├── CharacterNode.java
│ │ │ │ ├── GreekNamedays.java
│ │ │ │ ├── GreekSpecialNamedays.java
│ │ │ │ ├── NamedayCalendarProvider.java
│ │ │ │ ├── NamedayJSON.java
│ │ │ │ ├── NamedayJSONParser.java
│ │ │ │ ├── NamedayJSONProvider.java
│ │ │ │ ├── NamedayJSONResourceLoader.kt
│ │ │ │ ├── Node.java
│ │ │ │ ├── RomanianEasterSpecialCalculator.kt
│ │ │ │ ├── RomanianNamedays.kt
│ │ │ │ ├── RomanianSpecialNamedays.java
│ │ │ │ ├── SoundNode.kt
│ │ │ │ ├── SpecialGreekNamedaysCalculator.java
│ │ │ │ ├── SpecialNamedays.java
│ │ │ │ └── SpecialNamedaysHandlerFactory.java
│ │ │ └── peopleevents/
│ │ │ ├── ClosestEventsComparator.java
│ │ │ ├── CompositePeopleEventsProvider.kt
│ │ │ ├── ContactEventsOnADate.kt
│ │ │ ├── CustomEventType.kt
│ │ │ ├── EventType.kt
│ │ │ ├── NoEventsFoundException.kt
│ │ │ ├── PeopleDynamicNamedaysProvider.kt
│ │ │ ├── PeopleEventsPersister.kt
│ │ │ ├── PeopleEventsProvider.kt
│ │ │ ├── PeopleEventsRepository.kt
│ │ │ ├── PeopleEventsStaticEventsRefresher.kt
│ │ │ ├── PeopleEventsUpdater.kt
│ │ │ ├── ShortDateLabelCreator.java
│ │ │ ├── StandardEventType.java
│ │ │ ├── UpcomingEventsSettings.kt
│ │ │ └── UpcomingEventsViewRefresher.kt
│ │ ├── facebook/
│ │ │ ├── FacebookImagePath.kt
│ │ │ ├── FacebookUserSettings.kt
│ │ │ ├── UserCredentials.java
│ │ │ └── friendimport/
│ │ │ ├── CalendarFetcherException.java
│ │ │ ├── CalendarLoader.java
│ │ │ ├── ContactEventSerialiser.java
│ │ │ ├── FacebookBirthdaysProvider.java
│ │ │ ├── FacebookCalendarLoader.java
│ │ │ ├── FacebookContactFactory.java
│ │ │ └── InvalidFacebookContactException.java
│ │ ├── people/
│ │ │ ├── FacebookImportViewModel.kt
│ │ │ ├── NoContactsViewModel.kt
│ │ │ ├── PeoplePresenter.kt
│ │ │ ├── PeopleRowViewModel.kt
│ │ │ ├── PeopleView.java
│ │ │ ├── PeopleViewHolderListener.java
│ │ │ ├── PeopleViewModelFactory.kt
│ │ │ └── PersonViewModel.kt
│ │ ├── permissions/
│ │ │ └── MementoPermissions.kt
│ │ ├── person/
│ │ │ ├── AgeCalculator.kt
│ │ │ ├── ContactActions.kt
│ │ │ ├── ContactEventViewModel.kt
│ │ │ ├── EventViewModelFactory.kt
│ │ │ ├── PersonDetailItem.kt
│ │ │ └── StarSign.kt
│ │ ├── search/
│ │ │ ├── ContactEventLabelCreator.kt
│ │ │ ├── ContactWithEvents.java
│ │ │ ├── NameFilter.java
│ │ │ ├── NameMatcher.kt
│ │ │ └── PeopleEventsSearch.java
│ │ ├── ui/
│ │ │ └── widget/
│ │ │ └── LetterPainter.kt
│ │ ├── upcoming/
│ │ │ ├── AnnualDate.kt
│ │ │ ├── BankHolidayViewModel.kt
│ │ │ ├── CompositeUpcomingEventsProvider.kt
│ │ │ ├── ContactViewModelFactory.kt
│ │ │ ├── DateHeaderViewModel.kt
│ │ │ ├── MonthLabels.java
│ │ │ ├── UpcomingContactEventViewModel.kt
│ │ │ ├── UpcomingDateStringCreator.kt
│ │ │ ├── UpcomingEventRowViewModelFactory.kt
│ │ │ ├── UpcomingEventsAdRules.kt
│ │ │ ├── UpcomingEventsFreeUserAdRules.kt
│ │ │ ├── UpcomingEventsPresenter.kt
│ │ │ ├── UpcomingEventsProvider.kt
│ │ │ ├── UpcomingListMVPView.kt
│ │ │ ├── UpcomingNamedaysViewModel.kt
│ │ │ ├── UpcomingRowViewModel.kt
│ │ │ ├── UpcomingRowViewModelsBuilder.kt
│ │ │ ├── UpcomingRowViewType.java
│ │ │ └── widget/
│ │ │ ├── list/
│ │ │ │ └── NoAds.kt
│ │ │ └── today/
│ │ │ ├── PercentToValueConverter.java
│ │ │ ├── RecentPeopleEventsPresenter.kt
│ │ │ └── RecentPeopleEventsView.kt
│ │ └── util/
│ │ └── HashMapList.kt
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── alexstyl/
│ │ ├── TestColors.kt
│ │ ├── gsc/
│ │ │ ├── SoundComparerTest.kt
│ │ │ └── SoundTest.kt
│ │ └── specialdates/
│ │ ├── JavaStrings.kt
│ │ ├── NamesTest.kt
│ │ ├── OptionalTest.java
│ │ ├── TestContactEventsBuilder.kt
│ │ ├── TestDateLabelCreator.kt
│ │ ├── addevent/
│ │ │ ├── AddEventsPresenterTest.kt
│ │ │ ├── JavaEventIcons.kt
│ │ │ ├── JavaMessageDisplayer.kt
│ │ │ └── ui/
│ │ │ ├── ContactSourcesStubs.kt
│ │ │ └── ContactsSearchTest.kt
│ │ ├── contact/
│ │ │ ├── ContactCacheTest.java
│ │ │ ├── ContactFixture.java
│ │ │ ├── ContactTest.java
│ │ │ └── DisplayNameTest.java
│ │ ├── date/
│ │ │ └── ContactEventTest.java
│ │ ├── events/
│ │ │ ├── ContactActionTest.java
│ │ │ ├── DateTest.java
│ │ │ ├── ShortDateLabelCreatorTest.java
│ │ │ ├── StandardEventTypeTest.java
│ │ │ ├── bankholidays/
│ │ │ │ └── BankHolidayProviderTest.java
│ │ │ ├── namedays/
│ │ │ │ ├── NamedaysListTest.java
│ │ │ │ ├── OrthodoxEasterCalculatorTest.java
│ │ │ │ ├── RomanianNamedaysTest.java
│ │ │ │ ├── activity/
│ │ │ │ │ └── NamedaysInADayPresenterTest.kt
│ │ │ │ └── calendar/
│ │ │ │ └── resource/
│ │ │ │ ├── CharacterNodeTest.java
│ │ │ │ ├── GreeklishParserTest.java
│ │ │ │ ├── NamedayJSONParserTest.java
│ │ │ │ ├── RomanianEasterSpecialCalculatorTest.kt
│ │ │ │ ├── SoundNodeTest.kt
│ │ │ │ ├── TestJSONResourceLoader.kt
│ │ │ │ └── TestNamedayCalendarBuilder.java
│ │ │ └── peopleevents/
│ │ │ ├── ClosestEventsComparatorTest.java
│ │ │ ├── CompositePeopleEventsProviderTest.kt
│ │ │ ├── ContactEventsOnADateTest.kt
│ │ │ └── PeopleDynamicNamedaysProviderTest.kt
│ │ ├── facebook/
│ │ │ └── friendimport/
│ │ │ ├── FacebookBirthdaysProviderTest.java
│ │ │ ├── FacebookContactFactoryTest.java
│ │ │ ├── MockCalendarLoader.java
│ │ │ └── SystemLogTracker.kt
│ │ ├── person/
│ │ │ ├── AgeCalculatorTest.kt
│ │ │ └── StarSignTest.kt
│ │ ├── search/
│ │ │ ├── ContactEventTestBuilder.java
│ │ │ ├── EventLabelCreatorTest.java
│ │ │ ├── NameFilterTest.java
│ │ │ ├── NameMatcherTest.java
│ │ │ └── PeopleEventsSearchTest.kt
│ │ ├── timeofday/
│ │ │ └── TimeOfDayTest.java
│ │ ├── upcoming/
│ │ │ ├── MonthLabelsTest.java
│ │ │ ├── TimePeriodTest.java
│ │ │ ├── UpcomingEventsPresenterTest.kt
│ │ │ ├── UpcomingRowViewModelsBuilderTest.kt
│ │ │ └── widget/
│ │ │ └── today/
│ │ │ └── PercentToValueConverterTest.java
│ │ └── util/
│ │ ├── DateParserTest.kt
│ │ └── HashMapListTest.java
│ └── resources/
│ └── gr_namedays.json
├── secret.gradle
├── settings.gradle
├── team-props/
│ ├── android-code-quality.gradle
│ ├── static-analysis/
│ │ ├── checkstyle-modules.xml
│ │ ├── checkstyle-suppressions.xml
│ │ ├── detekt-config.yml
│ │ ├── findbugs-excludes.xml
│ │ ├── lint-config.xml
│ │ └── pmd-rules.xml
│ └── static-analysis.gradle
└── versions.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/ci-scripts/accept-android-licenses.sh
================================================
#!/usr/bin/env bash
export LICENSES_PATH="$ANDROID_HOME/licenses"
export ANDROID_SDK_LICENSE_PATH="$LICENSES_PATH/android-sdk-license"
export ANDROID_SDK_LICENSE_CONTENTS=$'\n8933bad161af4178b1185d1a37fbf41ea5269c55'
if [ ! -e ${ANDROID_SDK_LICENSE_PATH} ]; then
echo "Android SDK license acceptance not found in '$LICENSES_PATH', creating it..."
mkdir "$LICENSES_PATH" || true
echo -e "$ANDROID_SDK_LICENSE_CONTENTS" > "$ANDROID_SDK_LICENSE_PATH"
echo "Done."
else
echo "No need to create license acceptance file, already found in: $LICENSES_PATH/"
fi
================================================
FILE: .circleci/ci-scripts/ensure-sdkmanager.sh
================================================
#!/usr/bin/env bash
export TOOLS_BIN_PATH="$ANDROID_HOME/tools/bin"
export SDKMANAGER_PATH="$TOOLS_BIN_PATH/sdkmanager"
if [ ! -e ${SDKMANAGER_PATH} ]; then
echo "sdkmanager tool not found in '$SDKMANAGER_PATH', updating Android SDK tools..."
android update sdk --no-ui --all --filter tools
echo "Android SDK tools updated."
else
echo "No need to update Android SDK tools, sdkmanager found in: $TOOLS_BIN_PATH/"
fi
================================================
FILE: .circleci/ci-scripts/mock-google-services.json
================================================
{
"project_info": {
"project_id": "mockproject-1234",
"project_number": "123456789000",
"name": "FirebaseQuickstarts",
"firebase_url": "https://mockproject-1234.firebaseio.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:123456789000:android:f1bf012572b04063",
"client_id": "android:com.google.samples.quickstart.admobexample",
"client_type": 1,
"android_client_info": {
"package_name": "com.alexstyl.specialdates",
"certificate_hash": []
}
},
"oauth_client": [
{
"client_id": "123456789000-hjugbg6ud799v4c49dim8ce2usclthar.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.alexstyl.specialdates",
"certificate_hash": "4C20644DE36B8F89D25650C7D1FF9FBAE650FDF7"
}
},
{
"client_id": "123456789000-e4uksm38sne0bqrj6uvkbo4oiu4hvigl.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzbSzCn1N6LWIe6wthYyrgUUSAlUsdqMb-wvTo"
}
],
"services": {
"analytics_service": {
"status": 1
},
"cloud_messaging_service": {
"status": 2,
"apns_config": []
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "123456789000-e4uksm38sne0bqrj6uvkbo4oiu4hvigl.apps.googleusercontent.com",
"client_type": 3
}
]
},
"google_signin_service": {
"status": 2
},
"ads_service": {
"status": 2,
"test_banner_ad_unit_id": "ca-app-pub-3940256099942544/6300978111",
"test_interstitial_ad_unit_id": "ca-app-pub-3940256099942544/1033173712"
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:123456789000:android:f1bf012572b04063",
"client_id": "android:com.google.samples.quickstart.admobexample",
"client_type": 1,
"android_client_info": {
"package_name": "com.alexstyl.specialdates.pro",
"certificate_hash": []
}
},
"oauth_client": [
{
"client_id": "123456789000-hjugbg6ud799v4c49dim8ce2usclthar.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.alexstyl.specialdates.pro",
"certificate_hash": "4C20644DE36B8F89D25650C7D1FF9FBAE650FDF7"
}
},
{
"client_id": "123456789000-e4uksm38sne0bqrj6uvkbo4oiu4hvigl.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzbSzCn1N6LWIe6wthYyrgUUSAlUsdqMb-wvTo"
}
],
"services": {
"analytics_service": {
"status": 1
},
"cloud_messaging_service": {
"status": 2,
"apns_config": []
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "123456789000-e4uksm38sne0bqrj6uvkbo4oiu4hvigl.apps.googleusercontent.com",
"client_type": 3
}
]
},
"google_signin_service": {
"status": 2
},
"ads_service": {
"status": 2,
"test_banner_ad_unit_id": "ca-app-pub-3940256099942544/6300978111",
"test_interstitial_ad_unit_id": "ca-app-pub-3940256099942544/1033173712"
}
}
}
],
"client_info": [],
"ARTIFACT_VERSION": "1"
}
================================================
FILE: .circleci/config.yml
================================================
version: 2
jobs:
build:
docker:
- image: circleci/android:api-27-alpha
environment:
ANDROID_HOME: /opt/android/sdk
APPLICATION_ID: com.alexstyl.specialdates
steps:
- checkout
# Restore cached dependencies (if any)
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "android_mobile/build.gradle" }}
# Prepare the container for the build
- run:
name: Accept Android SDK license
command: .circleci/ci-scripts/accept-android-licenses.sh
- run:
name: Ensure Android SDK install is up-to-date
command: .circleci/ci-scripts/ensure-sdkmanager.sh
# Run the main job command, delegating to Gradle
- run:
name: Run Gradle :check command
command: ./gradlew check --stacktrace --continue
# Store all the downloaded dependencies in the CI cache
- save_cache:
paths:
# Android SDK
- /usr/local/android-sdk-linux/tools
- /usr/local/android-sdk-linux/platform-tools
- /usr/local/android-sdk-linux/build-tools
- /usr/local/android-sdk-linux/licenses
- /usr/local/android-sdk-linux/extras/google/m2repository
# Gradle dependencies
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "android_mobile/build.gradle" }}
# Collect static analysis reports as build artifacts
- store_artifacts:
path: android_mobile/build/reports
destination: reports
# Collect JUnit test results
- store_test_results:
path: android_mobile/build/test-results
================================================
FILE: .github/CONTRIBUTING.md
================================================
## Ways of contributing
There are a few ways of contributing into the project:
1. Bug fixes
2. Check the [Issues page](https://github.com/alexstyl/MementoNamedays/issues) and resolve any issues you can find
3. Code refactor. The project is fairly old, so there is huge room for improvement :)
4. Introduce any interesting functionality you might need. Please do let me know before doing this first. There might be a chance that a feature you might want to include be really specific to your needs and not fit the overall style of the app. For any questions on features PR, email me at alexstyl.dev@gmail.com
5. Translate the app [by visiting OneSky](https://memento.oneskyapp.com/admin/project/dashboard/project/85177)
## Creating Pull Requests (PR)
The repo contains two main branches:
* `master` which contains all changes currently available to the Play Store.
* `develop` which contains all changes to be uploaded in the next version of Memento.
1. Create a branch from `develop`.
2. Make any modifications required.
3. When done, open a PR of that branch against develop.
Make sure to consider testing the code you push.
Do not create huge PRs. If a PR grows big, make sure to create a new branch with those changes, and create a PR against your initial `feature` branch. The bigger the PR, the less chances of it getting merged on time.
## Reporting Issues
In case you found a bug in the app, or some bits in the code base that can cause issues, you can open an Issue so that someone can fix it. If it is a bug, make sure to write down all the steps it took to replicate it and (if possible) add a GIF showcasing the bug.
You can record your screen with the help of Android Studio ([see this video](https://youtu.be/uOvL4TXzOm4?t=39s)). Keep in mind that video recording does not work on Emulators, so you will need a real device for that. After you are done recording, convert the video into a GIF. I usually use this [website (video to gif)](http://image.online-convert.com/convert-to-gif). Make sure to attach the GIF into your Issue :)
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
#### Short Description
_Give a brief description of what the issue is_
##### Steps to reproduce
_This is just an example of how to write the steps_
1. Open app from home screen's icon
2. Select any day on the list
3. Tap on a contact's card
##### Resulted in
App crashes
##### Expected Results
Display the contact details of the selected contact
##### Video/Screenshots
(if no visual change, just delete)
{before image}
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
#### Description
_Give a brief explanation of what is the problem/feature you are working on and what this PR contains_
##### Test(s) added
_yes|no_ and reasoning why
##### Screenshots
(if no visual change, just delete)
| Before | After |
| ------ | ----- |
| {before image} | {after image} |
================================================
FILE: .gitignore
================================================
# Built application files
*.apk
*.ap_
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# Intellij
.idea/
*.iml
*.iws
# Keystore files
*.jks
# Mac
.DS_Store
# secrets
google-services.json
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2016 Alex Styl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: PEOPLE.md
================================================
Memento Calendar could not be in the amazing shape it is today if it wasn't for the following amazing people (alphabetically sorted):
* [auricgoldfinger](https://github.com/alexstyl/Memento-Calendar/pulls?utf8=%E2%9C%93&q=author%3Aauricgoldfinger) for the Big Text notification implementation
* [Andreas Sfakianakis](https://github.com/exaila) for the creation of the greeklish sound converter
* [Chrysa Papadopoulou](https://github.com/alexstyl/Memento-Calendar/pulls?utf8=%E2%9C%93&q=author%3Apchrysa) for the italian namedays
* [Daniele Bonaldo](https://github.com/alexstyl/Memento-Calendar/pulls?utf8=%E2%9C%93&q=author%3Adanybony) for the Android Wear implementation
* [Daniele Conti](https://github.com/alexstyl/Memento-Calendar/pulls?utf8=%E2%9C%93&q=author%3Afourlastor) for performance improvements and kotlin enhancements
* [Thanos Psaridis (Fisherman)](https://github.com/alexstyl/Memento-Calendar/pulls?utf8=%E2%9C%93&q=author%3AThanosFisherman) for bug fixing
* [Qi Qu](https://github.com/alexstyl/Memento-Calendar/pulls?utf8=%E2%9C%93&q=author%3Aqqipp) for the updated Memento Calendar app icon
* [madlymad](https://github.com/alexstyl/Memento-Calendar/pulls?utf8=%E2%9C%93&q=author%3Amadlymad) for improvements in the CI and madfixes
You can find all contributors at https://github.com/alexstyl/Memento-Calendar/graphs/contributors
Big thanks to all the translators via [OneSky](https://memento.oneskyapp.com/collaboration/project/85177):
* Andrejs Kotovs for the 🇱🇻 Latvian translations
* Aggela Styl for the 🇫🇷 French translations
* Bert for the 🇳🇱 Dutch translations
* Denis Mone for the 🇬🇷 Greek translations (corrections and improvements)
* Giangi for the 🇮🇹 Italian translations
* janfelcman for the 🇨🇿 Czech translations
================================================
FILE: README.md
================================================
# Archived
The repo is now archived and no new commits will take place. Thank for your support and contributions.
# Memento Calendar for Android [](https://travis-ci.org/alexstyl/Memento-Calendar)
Memento Calendar is a modern namedays app for Android.
This repository contains the source code of Memento Calendar.
You can get started by having a look at the project's wiki. It contains some information about how to get Memento up and running on your machine and other useful info.
This repo is open for PRs and they are more than welcome! Have a look [at the wiki page to see how to contribute](https://github.com/alexstyl/Memento-Calendar/wiki/How-to-contribute).
[](https://play.google.com/store/apps/details?id=com.alexstyl.specialdates)
## Project Goal
Memento Calendar is my pet project/playground in which I experiment with various platform features development patterns and share my foundings with the community my foundings via blog posts and talks. Memento started off as a side project app back in 2014 and has been on development on and off. Current goal of the project is to split out the business logic of the app from the app logic so that it could potentially be ported into other platforms with the help of Kotlin.
## Modules
The app is split into multiple modules.
The business logic of the app can be found in the **memento** module. There are three other Android Modules:
### android_common
This is the shared resources across all Android specific modules. It depends on *memento*.
### android_wear
This is the Android Wear module. It depends on *android_common*.
### android_mobile
This is the Android mobile app module. It depends on *android_common*.
## Architecture
The Model-View-Presenter is used in order to architecture the app.
**Presenters** are platform agnostic and live in the **memento** module, in order to be able to be used across all platforms. They contain the core logic of forwarding *Models* to the *Views*. It is up for the specific platform component to create a View
**Views** are responsible displaying information back to the user. For each view there is one interface that lives in the memento module. A view is not to be confused with Android's View classes, Activities or Fragments.
**Models** contain the minimum amount of information needed to render the information on the screen.
I did a talk in the GDG Android Athens about the structure of Memento Calendar. The talk is in Greek, but the slides contain more information about the structure [(see the slides)](https://speakerdeck.com/alexstyl/the-journey-towards-a-platform-agnostic-codebase).
## License
```
MIT License
Copyright (c) 2016 Alex Styl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
================================================
FILE: android_common/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply from: '../versions.gradle'
android {
compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion
// All these unused resources are actually used by other modules
lintOptions {
abortOnError false
}
defaultConfig {
minSdkVersion 16
buildConfigField 'String', 'MIXPANEL_TOKEN', '\"' + mixpanelToken + "\""
}
}
dependencies {
api project(path: ':memento')
api 'com.mixpanel.android:mixpanel-android:4.9.2'
api "com.android.support:support-annotations:$android_support_version"
api 'net.danlew:android.joda:2.9.9'
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
api 'io.reactivex.rxjava2:rxandroid:2.0.1'
}
================================================
FILE: android_common/src/main/AndroidManifest.xml
================================================
================================================
FILE: android_common/src/main/java/com/alexstyl/android/AndroidLogger.kt
================================================
package com.alexstyl.android
import android.util.Log
import com.alexstyl.Logger
class AndroidLogger : Logger {
override fun debug(message: String) {
Log.d(this.javaClass.simpleName, message)
}
override fun warning(message: String) {
Log.w(this.javaClass.simpleName, message)
}
}
================================================
FILE: android_common/src/main/java/com/alexstyl/android/Version.kt
================================================
package com.alexstyl.android
import android.os.Build
object Version {
fun hasJellyBean(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
}
fun hasKitKat(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
}
fun hasLollipop(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
}
fun hasMarshmallow(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}
fun hasOreo(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
}
}
================================================
FILE: android_common/src/main/java/com/alexstyl/android/ViewVisibility.java
================================================
package com.alexstyl.android;
import android.support.annotation.IntDef;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
View.VISIBLE,
View.INVISIBLE,
View.GONE
})
public @interface ViewVisibility {
}
================================================
FILE: android_common/src/main/java/com/alexstyl/android/preferences/widget/TimePreference.java
================================================
package com.alexstyl.android.preferences.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.DialogPreference;
import android.preference.PreferenceManager;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TimePicker;
import com.alexstyl.specialdates.common.R;
public class TimePreference extends DialogPreference {
private int lastHour = 0;
private int lastMinute = 0;
private TimePicker picker;
public TimePreference(Context ctx, AttributeSet attrs) {
super(ctx, attrs);
setDialogTitle(ctx.getString(R.string.set_time));
setPositiveButtonText(ctx.getString(R.string.set));
setNegativeButtonText(null);
}
@Override
protected View onCreateDialogView() {
picker = new TimePicker(getContext());
picker.setIs24HourView(DateFormat.is24HourFormat(getContext()));
return picker;
}
@Override
protected void onBindDialogView(View v) {
super.onBindDialogView(v);
picker.setCurrentHour(lastHour);
picker.setCurrentMinute(lastMinute);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult) {
lastHour = picker.getCurrentHour();
lastMinute = picker.getCurrentMinute();
int[] time = new int[2];
time[0] = lastHour;
time[1] = lastMinute;
if (callChangeListener(time)) {
persistString(time[0] + ":" + time[1]);
}
}
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return (a.getString(index));
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
String time;
if (restoreValue) {
if (defaultValue == null) {
time = PreferenceManager.getDefaultSharedPreferences(getContext()).getString(
getKey(), "00:00");
} else {
time = getPersistedString(defaultValue.toString());
}
} else {
time = defaultValue.toString();
}
lastHour = getHour(time);
lastMinute = getMinute(time);
}
private static int getHour(String time) {
String[] pieces = time.split(":");
return Integer.parseInt(pieces[0]);
}
private static int getMinute(String time) {
String[] pieces = time.split(":");
return (Integer.parseInt(pieces[1]));
}
}
================================================
FILE: android_common/src/main/java/com/alexstyl/android/widget/AppWidgetId.java
================================================
package com.alexstyl.android.widget;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A id of a Widget
*/
@Retention(RetentionPolicy.SOURCE)
public @interface AppWidgetId {
}
================================================
FILE: android_common/src/main/java/com/alexstyl/resources/AndroidDimensionResources.java
================================================
package com.alexstyl.resources;
import android.content.res.Resources;
import android.support.annotation.DimenRes;
final class AndroidDimensionResources implements DimensionResources {
private final Resources resources;
AndroidDimensionResources(Resources resources) {
this.resources = resources;
}
@Override
public int getPixelSize(@DimenRes int id) {
return resources.getDimensionPixelSize(id);
}
}
================================================
FILE: android_common/src/main/java/com/alexstyl/resources/DimensionResources.java
================================================
package com.alexstyl.resources;
import android.support.annotation.DimenRes;
import android.support.annotation.Px;
public interface DimensionResources {
@Px
int getPixelSize(@DimenRes int id);
}
================================================
FILE: android_common/src/main/java/com/alexstyl/specialdates/AndroidStrings.kt
================================================
package com.alexstyl.specialdates
import android.content.res.Resources
import com.alexstyl.specialdates.common.R
import com.alexstyl.specialdates.events.namedays.NamedayLocale
import com.alexstyl.specialdates.events.peopleevents.EventType
import com.alexstyl.specialdates.events.peopleevents.StandardEventType
import com.alexstyl.specialdates.person.StarSign
class AndroidStrings(private val resources: Resources) : Strings {
override fun contactAddedFailed(): String = resources.getString(R.string.failed_to_add_contact)
override fun contactAdded(): String = resources.getString(R.string.contact_added)
override fun contactUpdated(): String = resources.getString(R.string.contact_updated)
override fun contactUpdateFailed(): String = resources.getString(R.string.failed_to_update_contact)
override fun dontForgetToSendWishes(): String = resources.getString(R.string.Dont_forget_to_send_your_wishes)
override fun call(): String = resources.getString(R.string.Call)
override fun sendWishes(): String = resources.getString(R.string.Send_wishes)
override fun bankholidaySubtitle(): String = resources.getString(R.string.Bank_holiday_subtitle)
override fun contacts(): String = resources.getString(R.string.contacts)
override fun namedays(): String = resources.getString(R.string.namedays)
override fun bankholidays(): String = resources.getString(R.string.Bank_holidays)
override fun dailyReminder(): String = resources.getString(R.string.daily_reminder)
override fun postOnFacebook(): String = resources.getString(R.string.Post_on_Facebook)
override fun facebook(): String = resources.getString(R.string.Facebook)
override fun facebookMessenger(): String = resources.getString(R.string.facebook_messenger)
override fun viewConversation(): String = resources.getString(R.string.View_conversation)
override fun nameOf(starSign: StarSign): String = when (starSign) {
StarSign.AQUARIUS -> resources.getString(R.string.starsigns_aquarius)
StarSign.PISCES -> resources.getString(R.string.starsigns_pisces)
StarSign.ARIES -> resources.getString(R.string.starsigns_aries)
StarSign.TAURUS -> resources.getString(R.string.starsigns_taurus)
StarSign.GEMINI -> resources.getString(R.string.starsigns_gemini)
StarSign.CANCER -> resources.getString(R.string.starsigns_cancer)
StarSign.LEO -> resources.getString(R.string.starsigns_leo)
StarSign.VIRGO -> resources.getString(R.string.starsigns_virgo)
StarSign.LIBRA -> resources.getString(R.string.starsigns_libra)
StarSign.SCORPIO -> resources.getString(R.string.starsigns_scorpio)
StarSign.SAGITTARIUS -> resources.getString(R.string.starsigns_sagittarius)
StarSign.CAPRICORN -> resources.getString(R.string.starsigns_capricorn)
}
override fun turnsAge(age: Int): String = resources.getString(R.string.turns_age, age);
override fun inviteFriend(): String = resources.getString(R.string.Invite_friend)
override fun todaysNamedays(numberOfNamedays: Int): String = resources.getQuantityString(R.plurals.todays_nameday, numberOfNamedays)
override fun donateAmount(amount: String): String = resources.getString(R.string.donation_donate_amount, amount)
override fun eventOnDate(eventLabel: String, dateLabel: String): String = resources.getString(R.string.eventlabel_on_dateLabel, eventLabel, dateLabel)
override fun appName(): String = resources.getString(R.string.app_name)
override fun shareText(): String = resources.getString(R.string.share_text)
override fun today(): String = resources.getString(R.string.today)
override fun tomorrow(): String = resources.getString(R.string.tomorrow)
override fun todayCelebrateTwo(nameOne: String, nameTwo: String): String = resources.getString(R.string.today_celebrates_two, nameOne, nameTwo)
override fun todayCelebrateMany(name: String, numberLeft: Int): String = resources.getString(R.string.today_celebrates_many, name, numberLeft)
override fun nameOfEvent(event: EventType): String = when (event) {
StandardEventType.BIRTHDAY -> resources.getString(R.string.birthday)
StandardEventType.NAMEDAY -> resources.getString(R.string.nameday)
StandardEventType.ANNIVERSARY -> resources.getString(R.string.Anniversary)
StandardEventType.OTHER -> resources.getString(R.string.Other)
StandardEventType.CUSTOM -> resources.getString(R.string.Custom)
else -> {
throw IllegalStateException("$event has no name")
}
}
override fun localeName(locale: NamedayLocale): String = when (locale) {
NamedayLocale.GREEK -> resources.getString(R.string.Greek)
NamedayLocale.ROMANIAN -> resources.getString(R.string.Romanian)
NamedayLocale.RUSSIAN -> resources.getString(R.string.Russian)
NamedayLocale.LATVIAN -> resources.getString(R.string.Latvian_Traditional)
NamedayLocale.LATVIAN_EXTENDED -> resources.getString(R.string.Latvian_Extended)
NamedayLocale.SLOVAK -> resources.getString(R.string.Slovak)
NamedayLocale.ITALIAN -> resources.getString(R.string.Italian)
NamedayLocale.CZECH -> resources.getString(R.string.Czech)
NamedayLocale.HUNGARIAN -> resources.getString(R.string.Hungarian)
}
override fun viewFacebookProfile(): String = resources.getString(R.string.View_Facebook_Profile)
override fun importFromFacebook(): String = resources.getString(R.string.Import_from_Facebook)
}
================================================
FILE: android_common/src/main/java/com/alexstyl/specialdates/TextViewLabelSetter.java
================================================
package com.alexstyl.specialdates;
import android.widget.TextView;
public class TextViewLabelSetter implements LabelSetter {
private final TextView textView;
public TextViewLabelSetter(TextView textView) {
this.textView = textView;
}
@Override
public void setLabel(String text) {
textView.setText(text);
}
}
================================================
FILE: android_common/src/main/java/com/alexstyl/specialdates/date/IntentDateExtensions.kt
================================================
package com.alexstyl.specialdates.date
import android.content.Intent
val EXTRA_DAY_OF_MONTH = "extra:day_of_month"
val EXTRA_MONTH = "extra:month"
val EXTRA_YEAR = "extra:year"
fun Intent.putExtraDate(date: Date): Intent {
return putExtra(EXTRA_DAY_OF_MONTH, date.dayOfMonth)
.putExtra(EXTRA_MONTH, date.month)
.putExtra(EXTRA_YEAR, date.year)
}
fun Intent.getDateExtraOrThrow(): Date {
val dayOfMonth = getExtraOrThrow(this, EXTRA_DAY_OF_MONTH)
@MonthInt val month = getExtraOrThrow(this, EXTRA_MONTH)
val year = getExtraOrThrow(this, EXTRA_YEAR)
return Date.on(dayOfMonth, month, year)
}
private fun getExtraOrThrow(intent: Intent, extra: String): Int {
val intExtra = intent.getIntExtra(extra, -1)
if (intExtra == -1) {
throw IllegalArgumentException("Passing Intent did not include extra [$extra]")
}
return intExtra
}
================================================
FILE: android_common/src/main/java/com/alexstyl/specialdates/events/database/EventColumns.java
================================================
package com.alexstyl.specialdates.events.database;
public interface EventColumns {
/**
* A value of {@link EventTypeId} that indicates the type of event
*/
String EVENT_TYPE = "event_type";
/**
* The id of the same event stored in the device's default database
*
Will be set to -1 if not available
*/
String DEVICE_EVENT_ID = "device_event_id";
String DATE = "date";
String SOURCE = "source";
/**
* A value to indicate whether the user is interested in seeing this event.
*
1 for visible, 0 for not-visible
*/
String VISIBLE = "visible";
}
================================================
FILE: android_common/src/main/java/com/alexstyl/specialdates/wear/SharedConstants.java
================================================
package com.alexstyl.specialdates.wear;
public final class SharedConstants {
public static final String NEXT_CONTACT_EVENTS_PATH = "/next-contact-events";
public static final String KEY_DATE = "key_date";
public static final String KEY_CONTACTS_NAMES = "key_contacts_names";
private SharedConstants() {
// not instantiable
}
}
================================================
FILE: android_common/src/main/res/values/colors.xml
================================================
#eeeeee#F6F6F6@android:color/white#CC33B5E5#414141#707070#55B2A5#c61d30#ffeebb#B71C1C#c61d30#99c61d30#aeb857#536173#df5948#547bca#e5ae4f#ffffff#9e1726#ffff4444@color/avatar_variant_1#ff33b5e5#595959#6a1b9a#99ffffff#46000000#263238#ff669900#3b5998#151515
================================================
FILE: android_common/src/main/res/values/strings-non-translatable.xml
================================================
@string/localised_app_nameMemento CalendaremailEmailgoo.gl/GxZd6MMathilda Dawson@string/birthday@string/birthdays@string/nameday@string/namedays@string/celebrates_one_namedays@string/todays_namedays
================================================
FILE: android_common/src/main/res/values/strings.xml
================================================
Memento CalendarTodayTomorrowBirthdayBirthdaysName DaysName Day"It's %s's birthday!""It's %s's nameday!"No name days for this dayNo contact has their birthday this daySettingsDisableEnable%1$s%1$s and %2$s%1$s and %2$d othersAboutDaily Reminder ServiceDisplay Name DaysName Days will be displayed for all of your contactsName Days will not be displayedContact the developerFound a bug? Have a suggestion? Let me know!ChoosenumberCallSend SMSSend e-mailNo application found in order to perform this actionSend e-mail viaDaily Reminder"Remind me for every day's events"SoundVibrateEvery day at %sSet timeSetDiscardContact addedContact could not be addedContact updatedContact could not be updated@string/upcomingSilentContacts celebrating this dayThere are no contacts celebrating this dayUpcomingTurns %1$dToday\'s NamedaysCheck out %1$s - a sweet looking birthdays and namedays reminder app for Android! Get it at %2$sShare viaLicencesDeveloped and designed by Alex Styl"Special thanks to
Andreas Sfakianakis for his work on the Greeklish library
Kosta Stoupas for the German translation
Aggela Stylianidou for the French translation
Gian Maria Calzolari for the Italian translation
Andrejs Kotovs for the Latvian translation"and *%s* for using and supporting the app! :)Something is wrong with the app? Got a suggestion? Send an email at %sYouOnOffThank you for supporting the appDonateLoading…SearchContactsNameday onShow moreCreate(No name)Include year?SaveAddNo birthday setNo nameday for the name %sEditAdd birthdaySearchandGreekItalianCzechSlovakRussianLatvian (Traditional)Latvian (Extended)HungarianNameday CalendarSilent"Memento - Namedays"ShareSupport the appOpen Facebook PageHow would you rate the app?"It\'s pretty much horrible""I don't like it""It's okay""It's good""I LOVE it!"RateMessage%1$s turns %2$dNo results found%d contacts celebrate todayToday\'s Namedays"Today's nameday""Today's namedays"%s via…Like PageRate the appLike Facebook PageNamedays for %1$sGet %1$s at %2$sYou can hide names you don\'t want to appear in the app, by long pressing on them.OK, got itNamedays will be displayed for stored contacts onlyNamedays will be displays for the namedays of all yearNamedays for Contacts onlyI hope you enjoy using the app, and it is useful for you. Here are some ways you can support the app:Translate the appTranslating the appVisit the following url from your computer browserCopy LinkLink copied to clipboardSelect the amount you would like to donateView contactNo birthday set"No contacts with events found.\nTap the '+' to add some""No contacts with special events found"Add birthdayBirthday dateContact nameThemesRomanianConfigure WidgetDoneTransparencyUse dark themeContact NamesBank holidayBank holidaysGoogle+ communityDisplay Bank holidaysCountryBankholidays are currently supported only for GreekClearSearch for contactsSearch for contacts or namedaysMemento requires your permission to read your contacts in order to display when they celebrate.Fact: Some people give free cake to those who wish them Happy Birthday. Use Memento to keep track of potential cake givers.Grant permissionUse all parts of names"The app will lookup namedays for all names in a contact (including surname).""The app will lookup namedays only in contacts\' given names (excluding surname)."%1$s on %2$sAnniversaryOtherCustomAdd eventChange photoRemove photoTake new photoPick existing photoDiscard changes?No events on this dateInvite friendFacebook Log InYour Friends will be here shortly :)An error came upTry again in a bitHi, %s!Import from FacebookView Facebook ProfileFriends update dailyMemento on FacebookLog outFacebook ProfileNameday on %sBirthday on %sTurns %1$d on %2$sAdd birthdayPost on FacebookRomanianOpen SourceLicencesDonate - Remove adsMemento on GithubImport BirthdaysNewAquariusPiscesAriesTaurusGeminiCancerLeoVirgoLibraScorpioSagittariusCapricornMessengerHomeMobileWorkView conversationDonate %s♥"Hi, I'm Alex Styl""Memento Calendar was developed in Greece for all those who don't want to forget their loved ones.\n\nIt feels great to see how well received Memento is both by users and devs! All these Play Store reviews and donations give me the strength to keep maintaining Memento with new features and polishing!"\n\nMemento is a free application, supported by ads and donations. Donating any amount, removes all ads and grants you my infinite gratitude ♥.Restore donationPlaced a donation, but still see ads? Tap hereChecking for donations"No donation found. Have you donated?"Select dateFacebookShowHideWelcome"Want to support Memento's development?"Tap to see more events.Advanced notification settingsChange sounds, vibration, lights and more.Send wishes"Don't forget to send your wishes.""Today's bank holiday"Load my wallpaperCloseOpacityApply
================================================
FILE: android_common/src/main/res/values-cs/strings.xml
================================================
Memento CalendarDnesZítraDatum narozeníNarozeninySvátkySvátek"%s má narozeniny""%s má svátek"Žádný svátek pro tento denŽádný kontakt nemá narozeninyNastaveníZakázatPovolit%1$s%1$s a %2$s%1$s a %2$d ostatníO aplikaciSlužba denního připomínáníZobrazit svátkyBudou zobrazeny svátky pro všechny Vaše kontaktySvátky nebudou zobrazenyKontakt na vývojářeNalezl/a jste chybu? Máte námět? Dejte mi vědět!VybratčísloZavolatPoslat SMSPoslat e-mailPro provedení této akce nebyla nalezena zádná aplikacePoslat emailemDenní připomínání"Připomínat každodenní události"Vyzváněcí tónVibraceKaždý den v %sČas připomínkyNastavitZrušitKontakt přidánKontakt nemohl být přidánKontakt aktualizovánKontakt nemohl být aktualizovánTichýKontakty slavící dnesŽádné kontakty slavící tento denNadcházejícíSlaví %1$dDnešní svátkyOzkoušejte %1$s - pěknou aplikaci pro připomínání narozenin a svátků pro Android! Získejte ji zde %2$sSdílet přesLicenceVývoj a design Alex Styl"O překlad se zasloužili:
Andreas Sfakianakis - řecký překlad
Kosta Stoupas - německý překlad
Aggele Stylianidou - francouzský překlad
Gian Maria Calzolari - italský překlad
Andrejs Kotovs - lotyšský překlad"a *%s* za používání a podporu aplikaceChyba v aplikaci? Máte nějaký podnět? Pošlete email na %sVyZapnoutVypnoutDěkuji za podporu aplikaceDarovatNahrávání...HledatKontaktySvátek dneVíceVytvořit(Beze jména)Zahrnout rok?UložitPřidatNarozeniny nenastavenyŽádné narozeniny pro %sUpravitHledatařeckýčeskýslovenskýruskýlotyšskýmaďarskýKalendář svátkůTichý"Připomínač svátků"SdíletAplikační podporaOtevřít stránku FacebookuJak aplikaci hodnotíte?"Je hrozná""Nemám ji moc rád""Je ok""Je dobrá""MILUJI ji!"HodnotitZpráva%1$s slaví %2$dŽádné výsledky nenalezenyDnes slaví %d konkaktyDnešní svátky"Dnešní svátek""Dnešní svátky"%s přes…Jako stranaOhodnotit aplikaciPochválit stránku FacebookuNarozeniny pro %1$sDostat %1$s v %2$sDlouhým stiskem můžete skrýt jména, která nechcete v aplikaci zobrazitOk, rozumímSvátky budou zobrazeny pouze pro uložené kontaktySvátky budou zobrazeny pro celý rokSvátky pouze pro kontaktyDoufám, že se Vám aplikace líbí a je Vám užitečná. Tady jsou nějaké možnosti jak ji můžete podpořit:Přeložit aplikaciPřekládání aplikaceNavštívit následující odkaz ze svého počítačeZkopírovat odkazOdkaz zkopírován do schránkyVyber částku, kterou chcete darovatZobrazit kontaktŽádné narozeniny nastaveny"Nenalezeny žádné kontakty se zvláštními událostmi\nPoklepejte na '+' pro přidání""Nenalezeny žádné kontakty se zvláštními událostmi"Přidat narozeninyDatum narozeníJménoMotivKonfigurace widgetuHotovoPrůhlednostTmavý motivKontaktní jménaStátní svátekStátní svátkyGoogle+ komunitaZobrazit státní svátkyStátní svátky pro zemiStátní svátky jsou v současnosti podporovány pouze pro ŘeckoVymazatHledat kontaktyHledat kontakty nebo svátkyMemento požaduje Vaše svolení číst Vaše kontakty, aby zobrazilo datum oslavy.Fakt: Někteří lidé darují dort těm, kteří jim přejí vše nejlepší. Využijte Memento k dokumentování potencionálních dárců dortu.Povolit přístup.Svátek %sNarozeniny %s%1$s slaví %2$dPřidat narozeninyOtevřít tabuliRumunsko
================================================
FILE: android_common/src/main/res/values-de/strings.xml
================================================
Memento GeburtstageHeuteMorgenGeburtstagGeburtstageNamenstageNamenstag"%s hat Geburtstag!""Es ist %s's Namenstag!"Keine Namenstage für heuteKeiner Ihrer Konatkte hat heute GeburtstagEinstellungenDeaktivierenAktivieren%1$s%1$s und %2$s%1$s und %2$d weitereÜberTägliche ErinnerungNamenstage anzeigenNamenstage werden für alle Ihre Kontakte angezeigtNamenstage werden nicht angezeigtDen Entwickler kontaktierenHaben Sie einen Bug gefunden? Oder Sie haben einen Vorschlag? Lassen Sie es uns wissen!WähleNummerAnrufenSMS schreibenEmail schreibenEs wurde keine Anwendung zum Durchführen dieser Aktion gefundenEmail schreiben viaTägliche Erinnerung"Tägliche Erinnerung für jedes Ereignis"KlingeltonVibrationJeden Tag um %sZeit festlegenFertigLautlosWird %1$dNachrichtNamenstag am %sGeburtstag am %sWird %1$d am %2$s
================================================
FILE: android_common/src/main/res/values-el/strings.xml
================================================
Γιορτές MementoΣήμεραΑύριοΓενέθλιαΓενέθλιαΓιορτέςΓιορτή"Είναι τα γενέθλια του/της %s!""Γιορτάζει ο/η %s!"Καμία ονομαστική εορτή για αυτήν την ημέραΚαμία επαφή δεν έχει γενέθλια αυτήν την ημέραΡυθμίσειςΑπενεργοποίησηΕνεργό%1$s%1$s και %2$s%1$s και %2$d άλλοιΣχετικάΥπηρεσία Καθημερινής ΥπενθύμισηςΕμφάνιση Ονομαστικών ΕορτώνΟι Ονομαστικές Εορτές εμφανίζονται για όλες τις επαφέςΟι Ονομαστικές Εορτές δεν θα εμφανίζονταιΕπικοινωνίαΠαρουσιάστηκε κάποιο πρόβλημα; Έχετε κάποια πρόταση για την εφαρμογή; Πατήστε εδώΕπιλογήαριθμούΚλήσηΑποστολή SMSΑποστολή e-mailΔεν βρέθηκε κάποια εγκατεστημένη εφαρμογή για να ολοκληρωθεί αυτή η ενέργειαΑποστολή e-mail μέσωΚαθημερινή Υπενθύμιση"Θύμιζέ μου τις γιορτές και τα γενέθλια της κάθε ημέρας"ΉχοςΔόνησηΚάθε μέρα στις %sΡύθμιση ΏραςΤέλοςΑκύρωσηΗ επαφή δημιουργήθηκεΗ επαφή δεν προστέθηκεΗ επαφή ανανεώθηκεΗ επαφή δεν ενημερώθηκεΑθόρυβοΕπαφές που γιορτάζουν αυτή την ημέραΔεν γιορτάζει καμία επαφή αυτή την ημέραΕπερχόμενεςΓίνεται %1$dΣημερινές ΕορτέςΔείτε το %1$s - μια μοντέρνα εφαρμογή για γενέθλια και ονομαστικές γιορτες επαφών για Android! Κατεβαστε το στο %2$sΜοιραστείτε μέσωΆδειεςΥλοποιήθηκε και σχεδιάστηκε από τον Alex Styl"Πολλά ευχαριστώ στους\n
\nAndreas Sfakianakis για την βιβλιοθήκη για τους Greeklish χαρακτήρες
\nKosta Stoupas για την Γερμανική μετάφραση
\nAggela Stylianidou για την Γαλλική μετάφραση"και *%s* που χρησιμοποιείται και υποστηρίζεται την εφαρμογή! :)Κάτι πάει στραβά με την εφαρμογή; Θέλετε κάτι νέο να προστεθεί; Στείλτε e-mail στο %sΕσάςΕνεργόΑνενεργόΣας ευχαριστώ για την υποστήριξηΔωρεάΆνοιγμα…ΑναζητήστεΕπαφέςΓιορτάζει στιςΕμφάνισε περισσότεραΔημιούργησε(Ακατανόμαστος)με έτος γέννησης;ΑποθήκευσηΠροσθήκηΧωρίς γενέθλιαΔεν υπάρχει ονομαστική εορτή για το όνομα %sΕπεξεργασίαΠροσθήκη γενεθλίωνΑναζήτησηκαιΕλληνικάΙταλικάΤσέχικαΣλοβάκικαΡώσικαΛατβικάΟυγγρικάΓλώσσα ΕορτολογίουΑθόρυβο"Γιορτές - Ονομαστικές Εορτές"ΜοιραστείτεΥποστηρίξτε την εφαρμογήΆνοιγα της Facebook σελίδαςΠως θα βαθμολογούσατε την εφαρμογή;"Είναι απαίσια""Δεν μου αρέσει""Είναι εντάξει""Πολύ καλή""Την ΛΑΤΡΕΥΩ!"ΒαθμολογείστεΜήνυμαΟ/η %1$s γίνεται %2$dΔεν βρέθηκαν αποτελέσματα%d επαφές γιορτάζουν σήμεραΣημερινές Εορτές"Η γιορτή της ημέρας""Οι γιορτές της ημέρας"%s μέσω…Κάντε Like στην σελίδαΒαθμολογίστε την εφαρμογήΚάντε Like στην σελίδαΟνομαστικές εορτές για %1$sΚατεβάστε το %1$s στο %2$sΜπορείτε να κρύψετε ονομαστικές εορτές, πατώντας παρατεταμένα πάντως τους.ΟΚ, το \'πιασα.Οι Ονομαστικές εορτές θα εμφανίζονται μόνο για τις αποθηκευμένες επαφέςΟι Ονομαστικές εορτές θα εμφανίζονται για όλες τις ημέρες του χρόνουΜόνο για επαφέςΕλπίζω να σας αρέσει η εφαρμογή και να σας είναι χρήσιμη. Αν θέλετε να υποστηρίξετε την εφαρμογή, μπορείτε να επιλέξετε κάτι από τα παρακάτωΜεταφράστε την εφαρμογήΜετάφραση την εφαρμογήΕπισκεφθείτε το url απο τον browser του υπολογιστή σαςΑντιγραφήΤο URL αντιγράφθηκε στο πρόχειροΕπιλέξτε το ποσό που θα θέλατε να προσφέρετεΠροβολή επαφήςΔεν έχει οριστεί ημερομηνία γενεθλίων"Δεν βρέθηκαν επαφές με εορτές. Πατήστε το '+' για προσθήκη""Δεν βρέθηκαν επαφές με εορτές"Προσθήκη γενεθλίωνΗμερομηνία γενεθλίωνΌνομα επαφήςΘέματαΡωμανικάΡύθμιση Γρ. ΣτοιχείουΑποθήκευσηΔιαφάνειαΣκοτεινό θέμα;Ονόματα επαφώνΑργίαΑργίεςΚοινότητα Google+Προβολή αργίωνΧώρα αργίωνΟι αργίες υποστηρίζονται μόνο για την Ελλάδα αυτή τη στιγμήΚαθάρισμαΑναζήτηση επαφώνΑναζήτηση επαφών ή εορτώνΤο Memento χρειάζεται την άδειά σας για να διαβάσει τις επαφές σας προκειμένου να μπορεί να σας πει πότε γιορτάζουν.Μερικοί άνθρωποι δίνουν δωρεάν γλυκό σε όποιον τους πει Χρόνια Πολλά. Χρησιμοποιήστε το Memento για να είσαστε ενήμεροι για το ποιοι γιορτάζουν.Δώστε άδειαΧρήση όλων των ονομάτων επαφών"Η εφαρμογή θα προσπαθήσει να βρει ονομαστικές εορτές από όλα τα ονόματα κάθε επαφής (συμπεριλαμβανομένου του επωνύμου)""Η εφαρμογή θα προσπαθήσει να βρει ονομαστικές εορτές μόνο από τα μικρά ονόματα κάθε επαφές (χωρίς το επώνυμο)"%1$s στις %2$sΕπέτειοιΆλλοΠροσθήκη γεγονότοςΑλλαγή φωτογραφίαςΑφαίρεση φωτογραφίαςΛήψη νέας φωτογραφίαςΕπιλογή υπάρχουσας φωτογραφίαςΑκύρωση αλλαγών;Δεν υπάρχει γεγονότα για αυτή την ημερομηνίαΠρόσκληση φίλουΣύνδεση στο FacebookΟι Φίλοι σας είναι καθοδόν :)Παρουσιάσθηκε κάποιο σφάλμαΔοκιμάστε ξανά σε λιγάκιΓεια σου, %s!Εισαγωγή από το FacebookΟι φίλοι ανανεώνονται καθημερινάΤο Memento στο FacebookΑποσύνδεσηFacebook προφίλΓιορτάζει την %sΓενέθλια την %sΓίνεται %1$d την %2$sΠροσθήκη γενεθλίωνΆνοιγμα ΤοίχουΡωμάνικαΠερισσότερες ρυθμίσειςΑλλάξτε τον ήχο, την δόνηση, το χρώμα του LED και άλλα."Μην ξεχάσετε να τους ευχηθείτε."
================================================
FILE: android_common/src/main/res/values-fr/strings.xml
================================================
Memento FêtesAujourd\'huiDemainAnniversaireAnniversairesFêtesFête"C'est l'anniversaire de %s!""C'est la fête de %s!"Pas de fête aujourd\'huiPas de contact ayant son anniversaire aujourd\'huiParamètresDésactivationActivation%1$s%1$s et %2$s%1$s et %2$d encoreÀ proposService de rappel quotidienApparition des fêtesLes fêtes se montreront pour tous les contactsLes fêtes ne se montreront pasCommuniquez avec le programmateurEst-ce qu\'il y a des problèmes? Est-ce que vous avez des suggestions concernant l\' application? Touchez iciChoisissezun numéro de téléphoneAppelEnvoi un messageEnvoi un courrielPas d\' application existante afin que cette action se termineEnvoi un courriel viaRappel quotidien"Rappelle-moi les fêtes et les anniversaires de chaque jour"SonVibrer au rappelChaque jour à %sRéglage de l\'heureFinSilencieuxContacts qui fêtent aujourd\'huiPas de contacts ayant sα fête ou son anniversaire aujourd\'huiAura %1$dVoyez %1$s - Une application moderne pour les anniversaires et les fêtes des contacts pour Android!Téléchargez-la à %2$sPartagez viaLicencesDéveloppée et dessinée par Alex Styl"Remerciements spéciaux\n
\nAndreas Sfakianakis pour son travail à la librairie de Greeklish
\nKosta Stoupas pour la traduction en allemand
\nAggela Stylianidou pour la traduction en français"et *%s* qui utilisez et supportez l\'application! :)Est-se qu\' il y a des problèmes concernant l\' application? Est-ce que vous avez des suggestions concernant l\' application? Envoyez un courriel à %sVousMerci de supporter l\'applicationRechercheretSilencieuxPartagerSupporter l\'applicationOuvrir FacebookComment évalueriez-vous l\'application?"C\'est un peu horrible""Je ne l'aime pas""C'est ok""C'est bien""Je l'adore!"EvaluerMessageAucun résultat trouvéSa fête est %sSon anniversaire est %sAura %1$d ans %2$sAjouter anniversaire
================================================
FILE: android_common/src/main/res/values-it/strings.xml
================================================
Memento CalendarOggiDomaniCompleannoCompleanniOnomasticiOnomastico"E' il compleanno di %s!""E' l'onomastico di %s!"Nessun onomastico in questo giornoNessun compleanno in questo giornoImpostazioniDisattivaAttiva%1$s%1$s e %2$s%1$s e altri %2$dInformazioniServizio di notifica giornalieraVisualizza onomasticiVerranno visualizzati gli onomastici per tutti i tuoi contattiGli onomastici non verranno visualizzatiContatta lo sviluppatoreHai trovato un bug? Hai un suggerimento? Fammelo sapere!SceglinumeroChiamaInvia SMSInvia e-mailNessuna applicazione trovata per questa azioneInvia e-mail conNotifica giornaliera"Notificami per ogni evento del giorno"SuoneriaVibrazioneOgni giorno alle %sImposta oraImpostaIgnoraContatto aggiuntoIl contatto non può essere aggiuntoContatto aggiornatoIl contatto non può essere aggiornatoSilenziosoContatti che celebrano oggiNessun contatto celebra oggiProssimamenteCompie %1$d anniOnomastici di oggiProva %1$s - una bella app di promemoria per compleanni e onomastici per Android! Installala da %2$sCondividi conLicenzeSviluppato e progettato da Alex Styl"Ringraziamenti speciali a
Andreas Sfakianakis per il suo lavoro alla libreria di translitterazione dal Greco ("Greeklish")
Kosta Stoupas per la traduzione in Tedesco
Aggela Stylianidou per la traduzione in Francese
Gian Maria Calzolari per la traduzione in Italiano
Andrejs Kotovs per la traduzione in Lituano"e *%s* perché usi e supporti l\'app! :)C\'è qualche cosa di errato nella app? Hai un suggerimento? Invia una e-mail a %sTuAttivoSpentoGrazie per supportare l\'appDonaSto caricando...CercaContattiOnomastico ilMostra altriCrea(Nessun nome)Includi l\'anno?SalvaAggiungiCompleanno non impostatoNessun onomastico per il nome %sModificaAggiungi compleannoCercaeGrecoItalianoCecoSlovaccoRussoLituanoUnghereseCalendario onomasticoSilenzioso"Memento - Onomastici"CondividiSupporta l\'appVai alla pagina FacebookQuanto valuteresti l\'app?"Pessima""Non mi piace""Va bene""Mi è piaciuta""L'adoro!"ValutazioneMessaggio%1$s compie %2$d anniNessun risultato trovato%d contatti celebrano oggiOnomastici di oggi"Onomastico di oggi""Onomastici di oggi"%s con…Mi piaceValuta l\'appMi piace su FacebookOnomastici del %1$sInstalla %1$s da %2$sPuoi nascondere I nomi che non vuoi visualizzare nella app premendoli a lungo.OK, ho capitoVerranno visualizzati gli onomastici solo per i contatti memorizzatiVerranno visualizzati tutti gli onomastici dell\'annoOnomastici solo per i contattiMi auguro che tu sia soddisfatto della app e che ti sia utile. Ecco alcuni modi per supportarmi:Tradurre l\'applicazionePer tradurre l\'applicazioneVisita l\'url seguente dal browser del tuo computerCopia il linkLink copiato negli appuntiScegli l\'importo che vuoi donareVisualizza il contattoCompleanno non impostato"Non ho trovato contatti con eventi speciali.\nTocca il '+' per aggiungerne""Non ho trovato contatti con eventi speciali"Aggiungi compleannoCompleannoNome contattoTemiRomenoConfigura il widgetFattoTrasparenzaTema scuro?Nomi dei contattiFestivitàFestivitàComunità Google+Visualizza FestivitàNazioneAl momento sono supportate solo le Festività grechePulisciRicerca dei contattiRicerca dei contatti o onomasticiL\'app necessita il tuo permesso di leggere i contatti per poter visualizzare le date.Alcune persone offrono una torta a chi gli augura Buon Compleanno. Usa questa app per tenere traccia dei potenziali donatori di torta.Concedi il permessoUsa sia il nome che il cognome"L'app cercherà gli onomastici per il nome completo dei contatti (cognome incluso)""L\'app cercherà gli onomastici per il solo nome dei contatti (cognome escluso)"%1$s il %2$sAnniversarioAltroPersonalizzatoAggiungi eventoCambia la fotoRimuovi la fotoScatta una nuova fotoScegli una foto esistenteAnnulli le modifiche?Nessun evento in questa dataInvita un amicoAccedi a FacebookI tuoi Amici saranno qui a breve :)Si è verificato un erroreRiprova tra pocoCiao, %s!Importa da FacebookAggiornamento Amici giornalmenteMemento su FacebookEsciProfilo FacebookOnomastico il %sCompleanno il %sCompie %1$d anni il %2$sAggiungi compleannoApri il diarioRumeno
================================================
FILE: android_common/src/main/res/values-lv/bools.xml
================================================
true
================================================
FILE: android_common/src/main/res/values-lv-rLV/strings.xml
================================================
Memento CalendarŠodienaRītdienDzimšanas dienaDzimšanas dienasVārda dienasVārda diena"%s dzimšanas diena!""%s vārda diena!"Šajā dienā nevienam nav vārda dienasŠajā dienā nevienai kontaktpersonai nav dzimšanas dienasIestatījumiIzslēgtIeslēgt%1$s%1$s un %2$s%1$s un %2$d citiParIkdienas atgādinājumiRādīt vārda dienasTiks rādītas vārda dienas atbilstoši uzstādījumiem zemākVārda dienas netiks rādītasSazināties ar izstrādātājuAtradi kļūdu? Ir ieteikumi? Padod man ziņu!IzvēlētiesnumursZvanītSūtīt SMSSūtīt e-pastuNav nevienas piemērotas lietotnes, lai paveiktu darbībuSūtīt e-pastu arIkdienas atgādinājums"Atgādināt par katras dienas notikumiem"Skaņas tonisVibrācijaKatru dienu plkst. %sUzstādīt laikuUzstādītAtmestKontaktpersona pievienotaKontaktpersona netika pievienotaKontaktpersona atjaunotaKontaktpersona netika atjaunotaKlusumsKontaktpersonas, kas svin šajā dienāKontaktos nav neviena, kas svin šajā dienāSvētku dienas drīzumāPaliek %1$dŠodienas vārda dienasIzmēģini %1$s - glīta dzimšanas dienu un vārda dienu atgādinājumu Android lietotne! Iegūsti to %2$sDalies arLicencesAlex Styl izstrāde un dizains"Īpaša pateicība\n
\nAndreas Sfakianakis par darbu ar grieķu resursiem
\nKosta Stoupas par vācu valodas tulkojumu
\nAggela Stylianidou par franču valodas tulkojumu
\nGian Maria Calzolari par itāļu valodas tulkojumu
\nAndrejs Kotovs par latviešu valodas tulkojumu"un *%s* par lietošanu un atbalstu! :)Kaut kas ir nepareizi lietotnē? Ir ieteikumi? Sūti e-pastu uz %stevIesl.Izsl.Pateicos par atbalstu lietotneiZiedotIelāde...MeklētKontaktpersonasVārda dienaRādīt vairākIzveidot(Nav vārda)Iekļaut gadskaitli?SaglabātPievienotDzimšanas diena nav norādīta%s vārda diena nav zināmaRediģētPievienot dzimšanas dienuMeklētunGrieķuItāļuČehuSlovākuKrievuLatviešuUngāruVārda dienu kalendārsKlusums"Memento - Vārda dienas"DalītiesAtbalsti lietotniAtvērt FacebookVēlies novērtēt lietotni?"Diezgan briesmīgi""Man nepatīk""Ir OK""Labi""Man ļoti patīk!"NovērtētĪsziņa%1$s paliek %2$dNekas nav atrasts%d kontaktpersonas šodien svinŠodienas vārda dienas"Šodienas vārda diena""Šodienas vārda dienas"%s ar…Like PageNovērtē lietotniLike Facebook PageVārda dienas %1$sIegūsti %1$s no %2$sIlgāk paturot, vari paslēpt vārdus, kurus nevēlies redzēt lietotnēLabi, sapratuTiks rādītas tikai kontaktpersonu vārda dienasTiks rādītas vārda dienas visām gada dienāmVārda dienas tikai kontaktpersonāmEs ceru, ka tev patīk šī lietotne. Tu vari atbalstīt lietotnes izstrādi šādos veidos:Tulkot lietotniLietotnes tulkošanaAtvērt šo saiti no datora pārlūkprogrammasKopēt saitiSaite ir nokopētaIzvēlies ziedojuma summuApskatīt kontaktpersonuDzimšanas datums nav uzstādīts"Kontaktpersonas ar svētku dienām nav atrastas.\n\nSpied '+', lai pievienotu""Kontaktpersonas ar svētku dienām nav atrastas"Pievienot dzimšanas dienuDzimšanas datumsKontaktpersonas vārdsTēmasRumāņuKonfigurēt logrīkuDarītsCaurspīdīgumsTumšā tēma?Kontaktpersonu vārdiValsts svētku dienaValsts svētku dienasGoogle+ kopienaRādīt valsts svētku dienasValstsPašlaik tiek atbalstītas tikai Grieķijas valsts svētku dienasDzēstMeklēt kontaktpersonasMeklēt kontaktpersonas vai vārda dienasMemento lietotnei nepieciešama piekļuve taviem kontaktiem, lai parādītu kontaktpersonu svētku dienas.Fakts: Daudzi cilvēki mēdz cienāt ar kūkām sveicējus dzimšanas dienā. Izmanto Memento, lai laicīgi uzzinātu par potenciālajiem cienastiem.Atļaut piekļuviIzmantot visas vārdu daļas"Vārda dienu piemeklēšanai lietotne izmantos visas kontaktpersonu vārda daļas, t.sk. uzvārdus""Lietotne neiekļaus kontaktpersonu uzvārdus meklējot vārda dienas"%1$s - %2$sJubilejaCitiPielāgotiPievienot notikumuMainīt fotoattēluNoņemt fotoattēluUzņemt jaunu fotoattēluAtlasīt jaunu fotoattēluAtcelt izmaiņas?Nav pasākumu šajā datumāAicināt draugusIeiet FacebookTavi draugi drīz būs te :)Radusies kļūda!Pēc brīža mēģini atkārtoti!Sveiki, %s!Importēt no FacebookDraugu atjaunājumi ik dienuMemento lietotne FacebookIzietFacebook profilsVārda diena %sDzimšanas diena %sPaliek %1$d vecs %2$sPievienot dzimšanas dienuApskatīt sienuRumāņu
================================================
FILE: android_common/src/main/res/values-nl/strings.xml
================================================
Memento CalendarVandaagMorgenVerjaardagVerjaardagenNaamdagenNaamdag"Het is de verjaardag van %s!""Het is de naamdag van %s!"Geen naamdagen op deze dagNiemand van je contacten heeft vandaag zijn verjaardag.InstellingenUitschakelenInschakelen%1$s%1$s en %2$s%1$s en %2$d anderenOverService voor dagelijkse herinneringenToon naamdagenNaamdagen zullen worden getoond voor al je contactpersonenNaamdagen zullen niet getoond wordenContacteer de ontwikkelaarEen bug gevonden? Een suggestie? Laat het me weten!KiesnummerBelStuur SMSStuur e-mailGeen app gevonden om deze actie uit te voerenStuur e-mail viaDagelijkse herinnering"Toon me dagelijks een herinnering voor de verjaardagen of naamdagen van de dag"BeltoonTrillenEvery day om %sTijd van herinneringopslaanAnnuleerContact toegevoegdContact kon niet toegevoegd wordenContact bijgewerktContact kon niet bijgewerkt wordenStilContactpersonen die deze dag vierenEr zijn geen contactpersonen die deze dag vierenEerstvolgendeWordt %1$dNaamdagen van vandaagJe moet %1$s eens bekijken - een knappe app voor Android die herinneringen toont voor verjaardagen en naamdagen! Download van %2$sDelen viaLicentiesOntwikkeld en ontworpen door Alex Styl"Speciaal bedankt aan
Andreas Sfakianakis voor zijn werk aan de Greeklish bibliotheek
Kosta Stoupas voor de Duitse vertaling
Aggela Stylianidou voor de Franse vertaling
Gian Maria Calzolari voor de Italiaanse vertaling
Andrejs Kotovs voor de Letse vertaling
Bert Van Dooren voor de Nederlandse vertaling"en *%s* om deze app te gebruiken en te ondersteunen! :)Is er iets mis met de app? Heb je een suggestie? Stuur een e-mail naar %sJIJAanUitBedankt om deze app te ondersteunenDoneerBezig met laden...ZoekenContactpersonenNaamdag opToon meerAanmaken(Geen naam)Jaar toevoegen?OpslaanToevoegenGeen verjaardag gezetGeen naamdag voor de naam %sBewerkenVerjaardag toevoegenZoekenenGrieksItaliaansTsjechischSlovaaksRussischLetsHongaarsNaamdag kalenderStil"Memento - Naamdagen"DelenOndersteun de appOpen de Facebook PaginaWelke waardering zou je de app geven?"Het is echt afgrijselijk""Ik hou er niet van""'t Is wel ok""Het is goed""Ik kan niet meer zonder!"Waardering gevenBericht%1$s wordt %2$dGeen resultaten gevonden%d contactpersonen vieren vandaagNaamdagen van vandaag"Naamdag van vandaag""Naamdagen van vandaag"%s via...Like de paginaGeef de app een waarderingLike de Facebook paginaNaamdagen voor %1$sDownload %1$s op %2$sJe kan de namen verbergen die je niet wil zien in de app door er lang op te duwen.Ok, verstaan.Enkel de naamdagen van je contactpersonen zullen worden getoondAlle naamdagen van het jaar zullen getoond wordenEnkel naamdagen voor contactpersonenIk hoop dat je de app leuk en bruikbaar vindt. Hier zijn enkele manieren om de app de ondersteunen:Vertaal de appDe app vertalenOpen de volgende link op je computerKopiëer de linkLink gekopiëerd naar het klembordSelecteer hoeveel je wil donerenToon contactpersoonGeen verjaardag gevonden"Geen contactpersonen met speciale gebeurtenissen gevonden. \nDuw op de '+' om er toe te voegen""Geen contacten met speciale gebeurtenissen gevonden"Verjaardag toevoegenGeboortedatumNaam contactpersoonThema\'sRoemeensStel widget inKlaarTransparantieDonker thema?Namen contactpersonenVerlofdagVerlofdagenGoogle+ gemeenschapToon verlofdagenLandMomenteel zijn enkel Griekse verlofdagen ondersteundLeegmakenContacten zoekenContacten of naamdagen zoekenMemento heeft je goedkeuring nodig om je contacten in te lezen en zo te kunnen tonen wanneer ze gevierd worden."Feit: sommige mensen geven een gratis taart aan zij die hen Gelukkige Verjaardag wensen. Gebruik Memento om zulke potentiële taartgevers in het oog te houden. "Toestemming verlenenGebruik alle delen van namen"De app zal de naamdagen opzoeken voor alle namen in een contact (inclusief achternaam)""De app zal naamdagen enkel op basis van de voornaam van de contacten opzoeken (exclusief achternaam)"%1$s op %2$sJubileumAndereAangepastGebeurtenis toevoegenFoto wijzigenFoto verwijderenNieuwe foto nemenKies bestaande fotoWijzigingen verwerpen?Geen gebeurtenissen op deze datumVriend uitnodigenAanmelden met FacebookJe vrienden zullen hier weldra zijn :-)Er is iets fout gegaanWacht even en probeer nog eensHey, %s!Importeren van FacebookVrienden worden dagelijks geüpdatetMemento op FacebookAfmeldenFacebook profielNaamdag op %sVerjaardag op %sWordt %1$d op %2$sVerjaardag toevoegenOpen PrikbordRoemeens
================================================
FILE: android_common/src/main/res/values-sk/bool.xml
================================================
true
================================================
FILE: android_mobile/.gitignore
================================================
/build
================================================
FILE: android_mobile/build.gradle
================================================
plugins {
id "io.gitlab.arturbosch.detekt" version "1.0.0.M13.2"
}
apply from: '../versions.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'io.fabric'
detekt {
version = "1.0.0.M13.2"
profile("main") {
input = "$projectDir/src/main/java"
config = "$rootDir/team-props/static-analysis/detekt-config.yml"
filters = ".*test.*,.*/resources/.*,.*/tmp/.*"
output = "$projectDir/build/reports/detekt.xml"
}
}
project.afterEvaluate {
check.dependsOn tasks['detektCheck']
}
android {
compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion
applicationVariants.all { variant ->
variant.outputs.all { output ->
output.outputFileName = new File(
"./../../../../../build/",
output.outputFileName.replace(".apk", "-${variant.versionName}.apk"))
}
}
defaultConfig {
applicationId 'com.alexstyl.specialdates'
minSdkVersion 16
targetSdkVersion 27
versionCode androidVersionCode
versionName androidVersionName
resValue "string", "admob_unit_id", "\"$adMobUnitId\""
buildConfigField "String", "FILE_PROVIDER", "\"com.alexstyl.specialdates.fileprovider\""
manifestPlaceholders = [crashlyticsApiKey: crashlyticsKey,
fileProvider : "com.alexstyl.specialdates.fileprovider"]
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
}
}
packagingOptions {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
}
lintOptions {
abortOnError false
}
testOptions {
unitTests.returnDefaultValues = true
}
}
dependencies {
implementation fileTree(dir: 'libs', exclude: 'android-support-v4.jar', include: ['*.jar'])
implementation project(':android_common')
implementation "com.android.support:design:$android_support_version"
implementation "com.android.support:cardview-v7:$android_support_version"
implementation "com.android.support:appcompat-v7:$android_support_version"
implementation "com.android.support:recyclerview-v7:$android_support_version"
implementation "com.android.support:transition:$android_support_version"
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support:multidex:1.0.3'
implementation 'com.google.dagger:dagger:2.9'
kapt 'com.google.dagger:dagger-compiler:2.9'
compileOnly 'javax.annotation:jsr250-api:1.0'
implementation 'com.novoda:notils:2.2.15'
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
implementation 'com.novoda:simple-chrome-custom-tabs:0.1.6'
implementation('com.crashlytics.sdk.android:crashlytics:2.6.7@aar') {
transitive = true
}
implementation "com.google.android.gms:play-services-wearable:$play_services_version"
implementation 'com.theartofdev.edmodo:android-image-cropper:2.3.1'
implementation "com.google.firebase:firebase-core:$play_services_version"
implementation "com.google.firebase:firebase-ads:$play_services_version"
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxjava:2.1.0'
implementation 'com.evernote:android-job:1.2.4'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5'
debugImplementation 'com.facebook.stetho:stetho:1.3.0'
testImplementation 'junit:junit:4.12'
testImplementation 'org.easytesting:fest-assert-core:2.0M10'
testImplementation 'org.mockito:mockito-core:1.10.19'
testImplementation 'joda-time:joda-time:2.9.4'
testImplementation 'org.json:json:20140107'
androidTestImplementation 'junit:junit:4.12'
}
apply from: "$rootDir/android_mobile/google_services.gradle"
apply plugin: 'com.google.gms.google-services'
================================================
FILE: android_mobile/google_services.gradle
================================================
task copyGoogleServicesData() {
doLast {
if (!file("$rootDir/android_mobile/google-services.json").exists()) {
copy {
from "$rootDir/.circleci/ci-scripts/mock-google-services.json"
into "$rootDir/android_mobile"
rename { String fileName ->
fileName.replace("mock-google-services.json", "google-services.json")
}
}
}
}
}
tasks.whenTaskAdded { task ->
if (task.name == 'processDebugGoogleServices' || task.name == 'processReleaseGoogleServices') {
task.dependsOn copyGoogleServicesData
}
}
================================================
FILE: android_mobile/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/alexstyl/android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Joda Time
-dontwarn org.joda.convert.**
-dontwarn org.joda.time.**
-keep class org.joda.time.** { *; }
-keep interface org.joda.time.** { *; }
-keep class com.crashlytics.** { *; }
-dontwarn com.crashlytics.**
-keepattributes SourceFile,LineNumberTable,Annotation
-keep class com.crashlytics.android.**
-keep class com.android.vending.billing.**
-keep class com.tozny.crypto.android.AesCbcWithIntegrity$PrngFixes$* { *; }
================================================
FILE: android_mobile/src/debug/AndroidManifest.xml
================================================
================================================
FILE: android_mobile/src/debug/assets/mock/facebook-calendar.ics
================================================
BEGIN:VCALENDAR
PRODID:-//Facebook//NONSGML Facebook Events V1.0//EN
X-WR-CALNAME:Friends' Birthdays
X-PUBLISHED-TTL:PT12H
X-ORIGINAL-URL:https://www.facebook.com/events/birthdays/
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
DTSTART:20171219
SUMMARY:Alexandros Stylianidis's birthday
RRULE:FREQ=YEARLY
DURATION:P1D
UID:b1358181263@facebook.com
END:VEVENT
BEGIN:VEVENT
DTSTART:20171219
SUMMARY:Alex Alex's birthday
RRULE:FREQ=YEARLY
DURATION:P1D
UID:b100010984206219@facebook.com
END:VEVENT
END:VCALENDAR
================================================
FILE: android_mobile/src/debug/java/com/alexstyl/specialdates/DebugAppComponent.java
================================================
package com.alexstyl.specialdates;
import com.alexstyl.resources.ResourcesModule;
import com.alexstyl.specialdates.contact.ContactsModule;
import com.alexstyl.specialdates.dailyreminder.DailyReminderModule;
import com.alexstyl.specialdates.date.DateModule;
import com.alexstyl.specialdates.debug.DebugFragment;
import com.alexstyl.specialdates.debug.DebugModule;
import com.alexstyl.specialdates.donate.DonateModule;
import com.alexstyl.specialdates.events.namedays.NamedayModule;
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsModule;
import com.alexstyl.specialdates.images.ImageModule;
import com.alexstyl.specialdates.person.PersonModule;
import com.alexstyl.specialdates.upcoming.UpcomingEventsModule;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {
AndroidApplicationModule.class,
NamedayModule.class,
PersonModule.class,
ContactsModule.class,
PeopleEventsModule.class,
DonateModule.class,
DebugModule.class,
UpcomingEventsModule.class,
DailyReminderModule.class,
ResourcesModule.class,
ImageModule.class,
DateModule.class})
public interface DebugAppComponent {
void inject(DebugFragment fragment);
}
================================================
FILE: android_mobile/src/debug/java/com/alexstyl/specialdates/DebugApplication.java
================================================
package com.alexstyl.specialdates;
import com.alexstyl.resources.ResourcesModule;
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsModule;
import com.alexstyl.specialdates.images.ImageModule;
public class DebugApplication extends MementoApplication {
private DebugAppComponent debugAppComponent;
@Override
public void onCreate() {
super.onCreate();
debugAppComponent =
DaggerDebugAppComponent.builder()
.androidApplicationModule(new AndroidApplicationModule(this))
.peopleEventsModule(new PeopleEventsModule(this))
.imageModule(new ImageModule(getResources()))
.resourcesModule(new ResourcesModule(this, getResources()))
.build();
}
@Override
protected void initialiseDependencies() {
super.initialiseDependencies();
new OptionalDependencies(this).initialise();
}
public DebugAppComponent getDebugAppComponent() {
return debugAppComponent;
}
}
================================================
FILE: android_mobile/src/debug/java/com/alexstyl/specialdates/OptionalDependencies.java
================================================
package com.alexstyl.specialdates;
import android.app.Application;
import android.content.Context;
import com.facebook.stetho.Stetho;
import com.squareup.leakcanary.LeakCanary;
class OptionalDependencies {
private final Context context;
OptionalDependencies(Context context) {
this.context = context.getApplicationContext();
}
void initialise() {
Stetho.initializeWithDefaults(context);
if (!LeakCanary.isInAnalyzerProcess(context)) {
LeakCanary.install((Application) context);
}
}
}
================================================
FILE: android_mobile/src/debug/java/com/alexstyl/specialdates/debug/DebugActivity.java
================================================
package com.alexstyl.specialdates.debug;
import android.os.Bundle;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.ui.base.ThemedMementoActivity;
public class DebugActivity extends ThemedMementoActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_debug);
}
}
================================================
FILE: android_mobile/src/debug/java/com/alexstyl/specialdates/debug/DebugFragment.kt
================================================
package com.alexstyl.specialdates.debug
import android.app.Activity
import android.app.DatePickerDialog
import android.content.ContentUris
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.preference.Preference
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.widget.Toast
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.DebugApplication
import com.alexstyl.specialdates.Optional
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.contact.ContactSource.SOURCE_DEVICE
import com.alexstyl.specialdates.contact.ContactsProvider
import com.alexstyl.specialdates.contact.DisplayName
import com.alexstyl.specialdates.dailyreminder.*
import com.alexstyl.specialdates.date.ContactEvent
import com.alexstyl.specialdates.date.Date
import com.alexstyl.specialdates.date.DateParser
import com.alexstyl.specialdates.donate.DebugDonationPreferences
import com.alexstyl.specialdates.donate.DonateMonitor
import com.alexstyl.specialdates.events.bankholidays.BankHoliday
import com.alexstyl.specialdates.events.namedays.NamedayUserSettings
import com.alexstyl.specialdates.events.namedays.NamesInADate
import com.alexstyl.specialdates.events.peopleevents.DebugPeopleEventsUpdater
import com.alexstyl.specialdates.events.peopleevents.StandardEventType
import com.alexstyl.specialdates.events.peopleevents.UpcomingEventsSettings
import com.alexstyl.specialdates.events.peopleevents.UpcomingEventsViewRefresher
import com.alexstyl.specialdates.facebook.friendimport.FacebookFriendsIntentService
import com.alexstyl.specialdates.facebook.login.FacebookLogInActivity
import com.alexstyl.specialdates.support.AskForSupport
import com.alexstyl.specialdates.ui.base.MementoPreferenceFragment
import com.alexstyl.specialdates.upcoming.widget.today.UpcomingWidgetConfigureActivity
import com.alexstyl.specialdates.wear.WearSyncUpcomingEventsView
import com.evernote.android.job.JobRequest
import java.net.URI
import java.util.Calendar
import javax.inject.Inject
class DebugFragment : MementoPreferenceFragment() {
var dailyReminderDebugPreferences: DailyReminderDebugPreferences? = null
@Inject set
var namedayUserSettings: NamedayUserSettings? = null
@Inject set
var contactsProvider: ContactsProvider? = null
@Inject set
var refresher: UpcomingEventsViewRefresher? = null
@Inject set
var tracker: CrashAndErrorTracker? = null
@Inject set
var monitor: DonateMonitor? = null
@Inject set
var upcomingEventsSettings: UpcomingEventsSettings? = null
@Inject set
var dateParser: DateParser? = null
@Inject set
var notifier: DailyReminderNotifier? = null
@Inject set
var peopleEventsUpdater: DebugPeopleEventsUpdater? = null
@Inject set
var dailyReminderViewModelFactory: DailyReminderViewModelFactory? = null
@Inject set
var askForSupport: AskForSupport? = null
@Inject set
private val onDailyReminderDateSelectedListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
val month1 = month + 1 // dialog picker months have 0 index
dailyReminderDebugPreferences!!.setSelectedDate(dayOfMonth, month1, year)
}
override fun onCreate(paramBundle: Bundle?) {
super.onCreate(paramBundle)
val debugAppComponent = (activity!!.application as DebugApplication).debugAppComponent
debugAppComponent.inject(this)
addPreferencesFromResource(R.xml.preference_debug)
dailyReminderDebugPreferences = DailyReminderDebugPreferences.newInstance(activity)
findPreference(R.string.key_debug_refresh_db)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
peopleEventsUpdater?.refresh()
showToast("Refreshing Database")
true
}
findPreference(R.string.key_debug_refresh_widget)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
refresher!!.refreshViews()
showToast("Widget(s) refreshed")
true
}
findPreference(R.string.key_debug_daily_reminder_date_enable)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
dailyReminderDebugPreferences!!.setEnabled(newValue as Boolean)
true
}
findPreference(R.string.key_debug_daily_reminder_date)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val today = dailyReminderDebugPreferences!!.selectedDate
val datePickerDialog = DatePickerDialog(
activity!!, onDailyReminderDateSelectedListener,
today.year, today.month - 1, today.dayOfMonth
)
datePickerDialog.show()
false
}
findPreference(R.string.key_debug_daily_reminder)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
JobRequest.Builder(DailyReminderJob.TAG)
.startNow()
.build()
.schedule()
showToast("Daily Reminder Triggered")
true
}
findPreference(R.string.key_debug_start_calendar)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
startDateIntent()
true
}
findPreference(R.string.key_debug_trigger_wear_service)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
WearSyncUpcomingEventsView(activity).reloadUpcomingEventsView()
true
}
findPreference(R.string.key_debug_reset_donations)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { preference ->
DebugDonationPreferences.newInstance(preference.context, monitor).reset()
Toast.makeText(preference.context, "Donations reset. You should see ads from now on", Toast.LENGTH_SHORT).show()
true
}
findPreference(R.string.key_debug_trigger_support)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { preference ->
DebugPreferences.newInstance(preference.context, R.string.pref_call_to_rate).wipe()
askForSupport!!.requestForRatingSooner()
val message = "Support triggered. You should now see a prompt to rate the app when you launch it"
showToast(message)
true
}
findPreference(R.string.key_debug_facebook)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(activity, FacebookLogInActivity::class.java)
startActivity(intent)
true
}
findPreference(R.string.key_debug_facebook_fetch_friends)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(activity, FacebookFriendsIntentService::class.java)
activity!!.startService(intent)
true
}
findPreference(R.string.key_debug_open_contact)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val contactPickerIntent = Intent(
Intent.ACTION_PICK,
ContactsContract.Contacts.CONTENT_URI
)
startActivityForResult(contactPickerIntent, RESULT_PICK_CONTACT)
true
}
findPreference(R.string.key_debug_trigger_daily_reminder_notification_one)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notifyForContacts(arrayListOf(
contactEventOn(Date.today().minusDay(365 * 10), Contact(123L, "Peter".toDisplayName(), URI.create("content://com.android.contacts/contacts/123"), SOURCE_DEVICE), StandardEventType.BIRTHDAY)
))
true
}
findPreference(R.string.key_debug_trigger_daily_reminder_notification_many)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notifyForContacts(arrayListOf(
contactEventOn(Date.today().minusDay(365 * 10), Contact(336L, "Peter".toDisplayName(), URI.create("content://com.android.contacts/contacts/336"), SOURCE_DEVICE), StandardEventType.NAMEDAY),
contactEventOn(Date.today().minusDay(365 * 10), Contact(123L, "Alex".toDisplayName(), URI.create("content://com.android.contacts/contacts/123"), SOURCE_DEVICE), StandardEventType.BIRTHDAY),
contactEventOn(Date.today().minusDay(365 * 10), Contact(108L, "Anna".toDisplayName(), URI.create("content://com.android.contacts/contacts/108"), SOURCE_DEVICE), StandardEventType.ANNIVERSARY),
contactEventOn(Date.today().minusDay(365 * 10), Contact(108L, "Anna".toDisplayName(), URI.create("content://com.android.contacts/contacts/108"), SOURCE_DEVICE), StandardEventType.OTHER)
))
true
}
findPreference(R.string.key_debug_trigger_namedays_notification)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notifier!!.notifyFor(
DailyReminderViewModel(
dailyReminderViewModelFactory!!.summaryOf(emptyList()),
emptyList(),
namedaysNotifications(
arrayListOf("NamedayTest", "Alex", "Bravo", "NamedaysRock"
, "Alex", "Bravo", "NamedaysRock"
, "Alex", "Bravo", "NamedaysRock"
, "Alex", "Bravo", "NamedaysRock")),
Optional.absent()
)
)
true
}
findPreference(R.string.key_debug_trigger_bank_holiday)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notifier!!.notifyFor(
DailyReminderViewModel(
dailyReminderViewModelFactory!!.summaryOf(emptyList()),
emptyList(),
Optional.absent(),
bankholidayNotification()
)
)
true
}
findPreference(R.string.key_debug_configure_widgets)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(activity, UpcomingWidgetConfigureActivity::class.java)
activity!!.startActivity(intent)
true
}
}
private fun notifyForContacts(contacts: ArrayList) {
val viewModels = contacts
.toViewModels()
notifier!!.notifyFor(
DailyReminderViewModel(
dailyReminderViewModelFactory!!.summaryOf(viewModels),
viewModels,
Optional.absent(),
Optional.absent()
)
)
}
private fun bankholidayNotification() = Optional(dailyReminderViewModelFactory!!.viewModelFor(BankHoliday("Test Bank Holiday", Date.today())))
private fun namedaysNotifications(arrayList: ArrayList): Optional =
Optional(dailyReminderViewModelFactory!!.viewModelFor(NamesInADate(Date.today(),
arrayList
)))
private fun contactEventOn(date: Date, contact: Contact, standardEventType: StandardEventType) = ContactEvent(Optional.absent(), standardEventType,
date, contact)
private fun showToast(message: String) {
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
}
private fun startDateIntent() {
val cal = Calendar.getInstance()
val builder = CalendarContract.CONTENT_URI.buildUpon()
builder.appendPath("time")
ContentUris.appendId(builder, cal.timeInMillis)
val intent = Intent(Intent.ACTION_VIEW)
.setData(builder.build())
startActivity(intent)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RESULT_PICK_CONTACT && resultCode == Activity.RESULT_OK) {
val intent = Intent(Intent.ACTION_VIEW)
val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, data.data!!.lastPathSegment.toString())
intent.data = uri
startActivity(intent)
}
}
companion object {
private const val RESULT_PICK_CONTACT = 4929
}
private fun String.toDisplayName(): DisplayName = DisplayName.from(this)
private fun ArrayList.toViewModels(): ArrayList {
val viewmodels = arrayListOf()
forEach {
viewmodels.add(dailyReminderViewModelFactory!!.viewModelFor(it.contact, listOf(it)))
}
return viewmodels
}
}
================================================
FILE: android_mobile/src/debug/java/com/alexstyl/specialdates/debug/DebugModule.java
================================================
package com.alexstyl.specialdates.debug;
import android.content.Context;
import com.alexstyl.specialdates.dailyreminder.DailyReminderDebugPreferences;
import com.alexstyl.specialdates.events.namedays.NamedayDatabaseRefresher;
import com.alexstyl.specialdates.events.peopleevents.DebugPeopleEventsUpdater;
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsStaticEventsRefresher;
import dagger.Module;
import dagger.Provides;
@Module
public class DebugModule {
@Provides
DebugPeopleEventsUpdater debugPeopleEventsUpdater(PeopleEventsStaticEventsRefresher peopleEventsStaticEventsRefresher,
NamedayDatabaseRefresher namedayDatabaseRefresher) {
return new DebugPeopleEventsUpdater(peopleEventsStaticEventsRefresher, namedayDatabaseRefresher);
}
@Provides
DailyReminderDebugPreferences debugPreferences(Context context) {
return DailyReminderDebugPreferences.newInstance(context);
}
}
================================================
FILE: android_mobile/src/debug/java/com/alexstyl/specialdates/debug/DebugPreferences.java
================================================
package com.alexstyl.specialdates.debug;
import android.content.Context;
import android.support.annotation.StringRes;
import com.alexstyl.specialdates.EasyPreferences;
final class DebugPreferences {
private final EasyPreferences preferences;
public static DebugPreferences newInstance(Context context, @StringRes int prefKey) {
return new DebugPreferences(EasyPreferences.createForPrivatePreferences(context, prefKey));
}
private DebugPreferences(EasyPreferences preferences) {
this.preferences = preferences;
}
void wipe() {
preferences.clear();
}
}
================================================
FILE: android_mobile/src/debug/java/com/alexstyl/specialdates/donate/DebugDonationPreferences.java
================================================
package com.alexstyl.specialdates.donate;
import android.content.Context;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.R;
public final class DebugDonationPreferences {
private final EasyPreferences easyPreferences;
private final DonateMonitor donateMonitor;
public static DebugDonationPreferences newInstance(Context context, DonateMonitor donateMonitor) {
return new DebugDonationPreferences(EasyPreferences.createForDefaultPreferences(context), donateMonitor);
}
private DebugDonationPreferences(EasyPreferences easyPreferences, DonateMonitor donateMonitor) {
this.easyPreferences = easyPreferences;
this.donateMonitor = donateMonitor;
}
public void reset() {
easyPreferences.setBoolean(R.string.key_has_donated, false);
donateMonitor.onDonationUpdated();
}
}
================================================
FILE: android_mobile/src/debug/java/com/alexstyl/specialdates/events/peopleevents/DebugPeopleEventsUpdater.java
================================================
package com.alexstyl.specialdates.events.peopleevents;
import com.alexstyl.specialdates.events.namedays.NamedayDatabaseRefresher;
public final class DebugPeopleEventsUpdater {
private final PeopleEventsStaticEventsRefresher peopleEventsStaticEventsRefresher;
private final NamedayDatabaseRefresher namedayDatabaseRefresher;
public DebugPeopleEventsUpdater(PeopleEventsStaticEventsRefresher peopleEventsStaticEventsRefresher,
NamedayDatabaseRefresher namedayDatabaseRefresher) {
this.peopleEventsStaticEventsRefresher = peopleEventsStaticEventsRefresher;
this.namedayDatabaseRefresher = namedayDatabaseRefresher;
}
public void refresh() {
peopleEventsStaticEventsRefresher.rebuildEvents();
namedayDatabaseRefresher.refreshNamedaysIfEnabled();
}
}
================================================
FILE: android_mobile/src/debug/res/layout/activity_debug.xml
================================================
================================================
FILE: android_mobile/src/debug/res/layout/debug_activity_animations.xml
================================================
================================================
FILE: android_mobile/src/debug/res/layout/debug_activity_mixing_colors.xml
================================================
================================================
FILE: android_mobile/src/main/AndroidManifest.xml
================================================
================================================
FILE: android_mobile/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl
================================================
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import android.os.Bundle;
/**
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
* This service provides the following features:
* 1. Provides a new API to get details of in-app items published for the app including
* price, type, title and description.
* 2. The purchase flow is synchronous and purchase information is available immediately
* after it completes.
* 3. Purchase information of in-app purchases is maintained within the Google Play system
* till the purchase is consumed.
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
* in-app items are consumable and thereafter can be purchased again.
* 5. An API to get current purchases of the user immediately. This will not contain any
* consumed purchases.
*
* All calls will give a response code with the following possible values
* RESULT_OK = 0 - success
* RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog
* RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down
* RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested
* RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase
* RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API
* RESULT_ERROR = 6 - Fatal error during the API action
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
*/
interface IInAppBillingService {
/**
* Checks support for the requested billing API version, package and in-app type.
* Minimum API version supported by this interface is 3.
* @param apiVersion billing API version that the app is using
* @param packageName the package name of the calling app
* @param type type of the in-app item being purchased ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @return RESULT_OK(0) on success and appropriate response code on failures.
*/
int isBillingSupported(int apiVersion, String packageName, String type);
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
* with a list JSON strings containing the productId, price, title and description.
* This API can be called with a maximum of 20 SKUs.
* @param apiVersion billing API version that the app is using
* @param packageName the package name of the calling app
* @param type of the in-app items ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku",
* "type" : "inapp",
* "price" : "$5.00",
* "price_currency": "USD",
* "price_amount_micros": 5000000,
* "title : "Example Title",
* "description" : "This is an example description" }'
*/
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
/**
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
* the type, a unique purchase token and an optional developer payload.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type of the in-app item being purchased ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
* codes on failures.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
*/
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
String developerPayload);
/**
* Returns the current SKUs owned by the user of the type and package name specified along with
* purchase information and a signature of the data to be validated.
* This will return all SKUs that have been purchased in V3 and managed items purchased using
* V1 and V2 that have not been consumed.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param type of the in-app items being requested ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
on failures.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
/**
* Consume the last purchase of the given SKU. This will result in this item being removed
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return RESULT_OK(0) if consumption succeeded, appropriate response codes on failures.
*/
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
/**
* This API is currently under development.
*/
int stub(int apiVersion, String packageName, String type);
/**
* Returns a pending intent to launch the purchase flow for upgrading or downgrading a
* subscription. The existing owned SKU(s) should be provided along with the new SKU that
* the user is upgrading or downgrading to.
* @param apiVersion billing API version that the app is using, must be 5 or later
* @param packageName package name of the calling app
* @param oldSkus the SKU(s) that the user is upgrading or downgrading from,
* if null or empty this method will behave like {@link #getBuyIntent}
* @param newSku the SKU that the user is upgrading or downgrading to
* @param type of the item being purchased, currently must be "subs"
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
* codes on failures.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
*/
Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName,
in List oldSkus, String newSku, String type, String developerPayload);
/**
* Returns a pending intent to launch the purchase flow for an in-app item. This method is
* a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams}
* parameter. This parameter is a Bundle of optional keys and values that affect the
* operation of the method.
* @param apiVersion billing API version that the app is using, must be 6 or later
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type of the in-app item being purchased ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param developerPayload optional argument to be sent back with the purchase information
* @extraParams a Bundle with the following optional keys:
* "skusToReplace" - List - an optional list of SKUs that the user is
* upgrading or downgrading from.
* Pass this field if the purchase is upgrading or downgrading
* existing subscriptions.
* The specified SKUs are replaced with the SKUs that the user is
* purchasing. Google Play replaces the specified SKUs at the start of
* the next billing cycle.
* "replaceSkusProration" - Boolean - whether the user should be credited for any unused
* subscription time on the SKUs they are upgrading or downgrading.
* If you set this field to true, Google Play swaps out the old SKUs
* and credits the user with the unused value of their subscription
* time on a pro-rated basis.
* Google Play applies this credit to the new subscription, and does
* not begin billing the user for the new subscription until after
* the credit is used up.
* If you set this field to false, the user does not receive credit for
* any unused subscription time and the recurrence date does not
* change.
* Default value is true. Ignored if you do not pass skusToReplace.
* "accountId" - String - an optional obfuscated string that is uniquely
* associated with the user's account in your app.
* If you pass this value, Google Play can use it to detect irregular
* activity, such as many devices making purchases on the same
* account in a short period of time.
* Do not use the developer ID or the user's Google ID for this field.
* In addition, this field should not contain the user's ID in
* cleartext.
* We recommend that you use a one-way hash to generate a string from
* the user's ID, and store the hashed string in this field.
* "vr" - Boolean - an optional flag indicating whether the returned intent
* should start a VR purchase flow. The apiVersion must also be 7 or
* later to use this flag.
*/
Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku,
String type, String developerPayload, in Bundle extraParams);
/**
* Returns the most recent purchase made by the user for each SKU, even if that purchase is
* expired, canceled, or consumed.
* @param apiVersion billing API version that the app is using, must be 6 or later
* @param packageName package name of the calling app
* @param type of the in-app items being requested ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param continuationToken to be set as null for the first call, if the number of owned
* skus is too large, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @param extraParams a Bundle with extra params that would be appended into http request
* query string. Not used at this moment. Reserved for future functionality.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value: RESULT_OK(0) if success,
* {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures.
*
* "INAPP_PURCHASE_ITEM_LIST" - ArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - ArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- ArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchaseHistory(int apiVersion, String packageName, String type,
String continuationToken, in Bundle extraParams);
/**
* This method is a variant of {@link #isBillingSupported}} that takes an additional
* {@code extraParams} parameter.
* @param apiVersion billing API version that the app is using, must be 7 or later
* @param packageName package name of the calling app
* @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs"
* for subscriptions)
* @param extraParams a Bundle with the following optional keys:
* "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams}
* supports returning a VR purchase flow.
* @return RESULT_OK(0) on success and appropriate response code on failures.
*/
int isBillingSupportedExtraParams(int apiVersion, String packageName, String type,
in Bundle extraParams);
}
================================================
FILE: android_mobile/src/main/java/android/support/v4/preference/PreferenceFragment.java
================================================
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v4.preference;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.support.v4.app.Fragment;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.widget.ListView;
import com.alexstyl.specialdates.R;
@SuppressWarnings("all")
public abstract class PreferenceFragment extends Fragment implements
PreferenceManagerCompat.OnPreferenceTreeClickListener {
private static final String PREFERENCES_TAG = "android:preferences";
private PreferenceManager mPreferenceManager;
private ListView mList;
private boolean mHavePrefs;
private boolean mInitDone;
/**
* The starting request code given out to preference framework.
*/
private static final int FIRST_REQUEST_CODE = 100;
private static final int MSG_BIND_PREFERENCES = 1;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_BIND_PREFERENCES:
bindPreferences();
break;
}
}
};
final private Runnable mRequestFocus = new Runnable() {
public void run() {
mList.focusableViewAvailable(mList);
}
};
/**
* Interface that PreferenceFragment's containing activity should
* implement to be able to process preference items that wish to
* switch to a new fragment.
*/
public interface OnPreferenceStartFragmentCallback {
/**
* Called when the user has clicked on a Preference that has
* a fragment class name associated with it. The implementation
* to should instantiate and switch to an instance of the given
* fragment.
*/
boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref);
}
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
mPreferenceManager = PreferenceManagerCompat.newInstance(getActivity(), FIRST_REQUEST_CODE);
PreferenceManagerCompat.setFragment(mPreferenceManager, this);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup,
Bundle paramBundle) {
return paramLayoutInflater.inflate(
R.layout.preference_list_fragment, paramViewGroup,
false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (mHavePrefs) {
bindPreferences();
}
mInitDone = true;
if (savedInstanceState != null) {
Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
if (container != null) {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.restoreHierarchyState(container);
}
}
}
}
@Override
public void onStart() {
super.onStart();
PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, this);
}
@Override
public void onStop() {
super.onStop();
PreferenceManagerCompat.dispatchActivityStop(mPreferenceManager);
PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, null);
}
@Override
public void onDestroyView() {
mList = null;
mHandler.removeCallbacks(mRequestFocus);
mHandler.removeMessages(MSG_BIND_PREFERENCES);
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
PreferenceManagerCompat.dispatchActivityDestroy(mPreferenceManager);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
Bundle container = new Bundle();
preferenceScreen.saveHierarchyState(container);
outState.putBundle(PREFERENCES_TAG, container);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
PreferenceManagerCompat.dispatchActivityResult(mPreferenceManager, requestCode, resultCode, data);
}
/**
* Returns the {@link PreferenceManager} used by this fragment.
* @return The {@link PreferenceManager}.
*/
public PreferenceManager getPreferenceManager() {
return mPreferenceManager;
}
/**
* Sets the root of the preference hierarchy that this fragment is showing.
*
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
*/
public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
if (PreferenceManagerCompat.setPreferences(mPreferenceManager, preferenceScreen) && preferenceScreen != null) {
mHavePrefs = true;
if (mInitDone) {
postBindPreferences();
}
}
}
/**
* Gets the root of the preference hierarchy that this fragment is showing.
*
* @return The {@link PreferenceScreen} that is the root of the preference
* hierarchy.
*/
public PreferenceScreen getPreferenceScreen() {
return PreferenceManagerCompat.getPreferenceScreen(mPreferenceManager);
}
/**
* Adds preferences from activities that match the given {@link Intent}.
*
* @param intent The {@link Intent} to query activities.
*/
public void addPreferencesFromIntent(Intent intent) {
requirePreferenceManager();
setPreferenceScreen(PreferenceManagerCompat.inflateFromIntent(mPreferenceManager, intent, getPreferenceScreen()));
}
/**
* Inflates the given XML resource and adds the preference hierarchy to the current
* preference hierarchy.
*
* @param preferencesResId The XML resource ID to inflate.
*/
public void addPreferencesFromResource(int preferencesResId) {
requirePreferenceManager();
setPreferenceScreen(PreferenceManagerCompat.inflateFromResource(mPreferenceManager, getActivity(),
preferencesResId, getPreferenceScreen()));
}
/**
* {@inheritDoc}
*/
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
Preference preference) {
//if (preference.getFragment() != null &&
if (
getActivity() instanceof OnPreferenceStartFragmentCallback) {
return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment(
this, preference);
}
return false;
}
/**
* Finds a {@link Preference} based on its key.
*
* @param key The key of the preference to retrieve.
* @return The {@link Preference} with the key, or null.
* @see PreferenceGroup#findPreference(CharSequence)
*/
public Preference findPreference(CharSequence key) {
if (mPreferenceManager == null) {
return null;
}
return mPreferenceManager.findPreference(key);
}
private void requirePreferenceManager() {
if (mPreferenceManager == null) {
throw new RuntimeException("This should be called after super.onCreate.");
}
}
private void postBindPreferences() {
if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
}
private void bindPreferences() {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.bind(getListView());
}
}
public ListView getListView() {
ensureList();
return mList;
}
private void ensureList() {
if (mList != null) {
return;
}
View root = getView();
if (root == null) {
throw new IllegalStateException("Content view not yet created");
}
View rawListView = root.findViewById(android.R.id.list);
if (!(rawListView instanceof ListView)) {
throw new RuntimeException(
"Content has view with id attribute 'android.R.id.list' "
+ "that is not a ListView class");
}
mList = (ListView)rawListView;
if (mList == null) {
throw new RuntimeException(
"Your content must have a ListView whose id attribute is " +
"'android.R.id.list'");
}
mList.setOnKeyListener(mListOnKeyListener);
mHandler.post(mRequestFocus);
}
private OnKeyListener mListOnKeyListener = new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
Object selectedItem = mList.getSelectedItem();
if (selectedItem instanceof Preference) {
@SuppressWarnings("unused")
View selectedView = mList.getSelectedView();
//return ((Preference)selectedItem).onKey(
// selectedView, keyCode, event);
return false;
}
return false;
}
};
}
================================================
FILE: android_mobile/src/main/java/android/support/v4/preference/PreferenceManagerCompat.java
================================================
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v4.preference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.util.Log;
public class PreferenceManagerCompat {
private static final String TAG = PreferenceManagerCompat.class.getSimpleName();
/**
* Interface definition for a callback to be invoked when a
* {@link Preference} in the hierarchy rooted at this {@link PreferenceScreen} is
* clicked.
*/
interface OnPreferenceTreeClickListener {
/**
* Called when a preference in the tree rooted at this
* {@link PreferenceScreen} has been clicked.
*
* @param preferenceScreen The {@link PreferenceScreen} that the
* preference is located in.
* @param preference The preference that was clicked.
* @return Whether the click was handled.
*/
boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference);
}
static PreferenceManager newInstance(Activity activity, int firstRequestCode) {
try {
Constructor c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class);
c.setAccessible(true);
return c.newInstance(activity, firstRequestCode);
} catch (Exception e) {
Log.w(TAG, "Couldn't call constructor PreferenceManager by reflection", e);
}
return null;
}
/**
* Sets the owning preference fragment
*/
static void setFragment(PreferenceManager manager, PreferenceFragment fragment) {
// stub
}
/**
* Sets the callback to be invoked when a {@link Preference} in the
* hierarchy rooted at this {@link PreferenceManager} is clicked.
*
* @param listener The callback to be invoked.
*/
static void setOnPreferenceTreeClickListener(PreferenceManager manager, final OnPreferenceTreeClickListener listener) {
try {
Field onPreferenceTreeClickListener = PreferenceManager.class.getDeclaredField("mOnPreferenceTreeClickListener");
onPreferenceTreeClickListener.setAccessible(true);
if (listener != null) {
Object proxy = Proxy.newProxyInstance(
onPreferenceTreeClickListener.getType().getClassLoader(),
new Class[] { onPreferenceTreeClickListener.getType() },
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) {
if (method.getName().equals("onPreferenceTreeClick")) {
return Boolean.valueOf(listener.onPreferenceTreeClick((PreferenceScreen) args[0], (Preference) args[1]));
} else {
return null;
}
}
});
onPreferenceTreeClickListener.set(manager, proxy);
} else {
onPreferenceTreeClickListener.set(manager, null);
}
} catch (Exception e) {
Log.w(TAG, "Couldn't set PreferenceManager.mOnPreferenceTreeClickListener by reflection", e);
}
}
/**
* Inflates a preference hierarchy from the preference hierarchies of
* {@link Activity Activities} that match the given {@link Intent}. An
* {@link Activity} defines its preference hierarchy with meta-data using
* the {@link #METADATA_KEY_PREFERENCES} key.
*
* If a preference hierarchy is given, the new preference hierarchies will
* be merged in.
*
* @param queryIntent The intent to match activities.
* @param rootPreferences Optional existing hierarchy to merge the new
* hierarchies into.
* @return The root hierarchy (if one was not provided, the new hierarchy's
* root).
*/
static PreferenceScreen inflateFromIntent(PreferenceManager manager, Intent intent, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, intent, screen);
return prefScreen;
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.inflateFromIntent by reflection", e);
}
return null;
}
/**
* Inflates a preference hierarchy from XML. If a preference hierarchy is
* given, the new preference hierarchies will be merged in.
*
* @param context The context of the resource.
* @param resId The resource ID of the XML to inflate.
* @param rootPreferences Optional existing hierarchy to merge the new
* hierarchies into.
* @return The root hierarchy (if one was not provided, the new hierarchy's
* root).
* @hide
*/
static PreferenceScreen inflateFromResource(PreferenceManager manager, Activity activity, int resId, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, activity, resId, screen);
return prefScreen;
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.inflateFromResource by reflection", e);
}
return null;
}
/**
* Returns the root of the preference hierarchy managed by this class.
*
* @return The {@link PreferenceScreen} object that is at the root of the hierarchy.
*/
static PreferenceScreen getPreferenceScreen(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen");
m.setAccessible(true);
return (PreferenceScreen) m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.getPreferenceScreen by reflection", e);
}
return null;
}
/**
* Called by the {@link PreferenceManager} to dispatch a subactivity result.
*/
static void dispatchActivityResult(PreferenceManager manager, int requestCode, int resultCode, Intent data) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class);
m.setAccessible(true);
m.invoke(manager, requestCode, resultCode, data);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityResult by reflection", e);
}
}
/**
* Called by the {@link PreferenceManager} to dispatch the activity stop
* event.
*/
static void dispatchActivityStop(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop");
m.setAccessible(true);
m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityStop by reflection", e);
}
}
/**
* Called by the {@link PreferenceManager} to dispatch the activity destroy
* event.
*/
static void dispatchActivityDestroy(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy");
m.setAccessible(true);
m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityDestroy by reflection", e);
}
}
/**
* Sets the root of the preference hierarchy.
*
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
* @return Whether the {@link PreferenceScreen} given is different than the previous.
*/
static boolean setPreferences(PreferenceManager manager, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class);
m.setAccessible(true);
return ((Boolean) m.invoke(manager, screen));
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.setPreferences by reflection", e);
}
return false;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/android/Bitmap.kt
================================================
package com.alexstyl.android
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import java.net.URI
fun Drawable.toBitmap(): Bitmap {
if (this is BitmapDrawable) {
return bitmap
}
val width = if (bounds.isEmpty) intrinsicWidth else bounds.width()
val height = if (bounds.isEmpty) intrinsicHeight else bounds.height()
return Bitmap.createBitmap(width.nonZero(), height.nonZero(), Bitmap.Config.ARGB_8888).also {
val canvas = Canvas(it)
setBounds(0, 0, canvas.width, canvas.height)
draw(canvas)
}
}
private fun Int.nonZero() = if (this <= 0) 1 else this
================================================
FILE: android_mobile/src/main/java/com/alexstyl/android/SimpleAnimatorListener.java
================================================
package com.alexstyl.android;
import android.animation.Animator;
public class SimpleAnimatorListener implements Animator.AnimatorListener {
@Override
public void onAnimationStart(Animator animator) {
// do nothing
}
@Override
public void onAnimationEnd(Animator animator) {
// do nothing
}
@Override
public void onAnimationCancel(Animator animator) {
// do nothing
}
@Override
public void onAnimationRepeat(Animator animator) {
// do nothing
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/android/Uri.kt
================================================
package com.alexstyl.android
import android.net.Uri
import java.net.URI
fun URI.toUri(): Uri = Uri.parse(this.toString())
fun Uri.toURI(): URI = URI.create(this.toString())
================================================
FILE: android_mobile/src/main/java/com/alexstyl/android/preferences/PreferenceKeyId.java
================================================
package com.alexstyl.android.preferences;
public @interface PreferenceKeyId {
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/resources/AndroidColors.kt
================================================
package com.alexstyl.resources
import android.content.Context
import android.support.v4.content.ContextCompat
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.events.database.EventTypeId
import com.alexstyl.specialdates.events.peopleevents.EventType
class AndroidColors(private val context: Context) : Colors {
override fun getColorFor(eventType: EventType) = when (eventType.id) {
EventTypeId.TYPE_BIRTHDAY -> ContextCompat.getColor(context, R.color.birthday_red)
EventTypeId.TYPE_NAMEDAY -> ContextCompat.getColor(context, R.color.nameday_blue)
EventTypeId.TYPE_ANNIVERSARY -> ContextCompat.getColor(context, R.color.anniversary_yellow)
EventTypeId.TYPE_CUSTOM -> ContextCompat.getColor(context, R.color.purple_custom_event)
EventTypeId.TYPE_OTHER -> ContextCompat.getColor(context, R.color.purple_custom_event)
else -> {
throw IllegalStateException("No color matching for $eventType")
}
}
override fun getDateHeaderTextColor(): Int = ContextCompat.getColor(context, R.color.upcoming_header_text_color)
override fun getTodayHeaderTextColor(): Int = ContextCompat.getColor(context, R.color.upcoming_header_today_text_color)
override fun getDailyReminderColor(): Int = ContextCompat.getColor(context, R.color.main_red)
override fun getNamedaysColor(): Int = ContextCompat.getColor(context, R.color.nameday_blue)
override fun getBankholidaysColor(): Int = ContextCompat.getColor(context, R.color.bankholiday_green)
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/resources/ResourcesModule.java
================================================
package com.alexstyl.resources;
import android.content.Context;
import android.content.res.Resources;
import com.alexstyl.specialdates.AndroidStrings;
import com.alexstyl.specialdates.Strings;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
@Singleton
public class ResourcesModule {
private final Resources resources;
private final Context context;
public ResourcesModule(Context context, Resources resources) {
this.context = context;
this.resources = resources;
}
@Provides
@Singleton
Strings providesString() {
return new AndroidStrings(resources);
}
@Provides
@Singleton
DimensionResources providesDimensionResources() {
return new AndroidDimensionResources(resources);
}
@Provides
@Singleton
Colors providesColorResources() {
return new AndroidColors(context);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/AndroidApplicationModule.java
================================================
package com.alexstyl.specialdates;
import android.app.AlarmManager;
import android.app.NotificationManager;
import android.appwidget.AppWidgetManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import com.alexstyl.Logger;
import com.alexstyl.android.AndroidLogger;
import com.alexstyl.specialdates.dailyreminder.AlarmManagerCompat;
import com.alexstyl.specialdates.events.database.EventSQLiteOpenHelper;
import com.alexstyl.specialdates.permissions.AndroidPermissions;
import com.alexstyl.specialdates.permissions.MementoPermissions;
import com.evernote.android.job.JobManager;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
class AndroidApplicationModule {
private final Context context;
AndroidApplicationModule(Context appContext) {
this.context = appContext;
}
@Provides
Context appContext() {
return context;
}
@Provides
Resources resources() {
return context.getResources();
}
@Provides
PackageManager pkgManager() {
return context.getPackageManager();
}
@Provides
ContentResolver contentResolver() {
return context.getContentResolver();
}
@Provides
AppWidgetManager appWidgetManager() {
return AppWidgetManager.getInstance(context);
}
@Provides
@Singleton
EventSQLiteOpenHelper sqLiteOpenHelper() {
return new EventSQLiteOpenHelper(context);
}
@Provides
MementoPermissions permissionChecker(CrashAndErrorTracker tracker) {
return new AndroidPermissions(tracker, context);
}
@Provides
CrashAndErrorTracker tracker() {
return new FabricTracker(context);
}
@Provides
@Singleton
JobManager jobManager() {
return JobManager.create(context);
}
@Provides
NotificationManager notificationManager() {
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
@Provides
Logger logger() {
return new AndroidLogger();
}
@Provides
AlarmManagerCompat alarmManagerCompat() {
return new AlarmManagerCompat((AlarmManager) context.getSystemService(Context.ALARM_SERVICE));
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/AppComponent.java
================================================
package com.alexstyl.specialdates;
import com.alexstyl.resources.ResourcesModule;
import com.alexstyl.specialdates.addevent.AddEventActivity;
import com.alexstyl.specialdates.addevent.AddEventModule;
import com.alexstyl.specialdates.addevent.EventDatePickerDialogFragment;
import com.alexstyl.specialdates.addevent.ui.ContactSuggestionView;
import com.alexstyl.specialdates.analytics.AnalyticsModule;
import com.alexstyl.specialdates.contact.ContactsModule;
import com.alexstyl.specialdates.dailyreminder.actions.PersonActionsActivity;
import com.alexstyl.specialdates.dailyreminder.DailyReminderModule;
import com.alexstyl.specialdates.dailyreminder.actions.ContactActionsModule;
import com.alexstyl.specialdates.date.DateModule;
import com.alexstyl.specialdates.donate.DonateActivity;
import com.alexstyl.specialdates.donate.DonateModule;
import com.alexstyl.specialdates.events.bankholidays.BankHolidaysModule;
import com.alexstyl.specialdates.events.namedays.NamedayModule;
import com.alexstyl.specialdates.events.namedays.activity.NamedaysOnADayActivity;
import com.alexstyl.specialdates.events.namedays.activity.NamedaysInADayModule;
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsModule;
import com.alexstyl.specialdates.facebook.FacebookModule;
import com.alexstyl.specialdates.facebook.FacebookProfileActivity;
import com.alexstyl.specialdates.facebook.friendimport.FacebookFriendsIntentService;
import com.alexstyl.specialdates.facebook.login.FacebookLogInActivity;
import com.alexstyl.specialdates.facebook.login.FacebookWebView;
import com.alexstyl.specialdates.home.HomeActivity;
import com.alexstyl.specialdates.images.ImageModule;
import com.alexstyl.specialdates.people.PeopleFragment;
import com.alexstyl.specialdates.people.PeopleModule;
import com.alexstyl.specialdates.permissions.ContactPermissionActivity;
import com.alexstyl.specialdates.person.PersonActivity;
import com.alexstyl.specialdates.person.PersonModule;
import com.alexstyl.specialdates.receiver.BootCompleteReceiver;
import com.alexstyl.specialdates.search.SearchActivity;
import com.alexstyl.specialdates.search.SearchModule;
import com.alexstyl.specialdates.settings.DailyReminderFragment;
import com.alexstyl.specialdates.settings.UserSettingsFragment;
import com.alexstyl.specialdates.settings.NamedayListPreference;
import com.alexstyl.specialdates.support.RateDialog;
import com.alexstyl.specialdates.theming.ThemingModule;
import com.alexstyl.specialdates.ui.base.ThemedMementoActivity;
import com.alexstyl.specialdates.ui.widget.ColorImageView;
import com.alexstyl.specialdates.ui.widget.ViewModule;
import com.alexstyl.specialdates.upcoming.UpcomingEventsFragment;
import com.alexstyl.specialdates.upcoming.UpcomingEventsModule;
import com.alexstyl.specialdates.upcoming.widget.RecentUpcomingPeopleEventsModule;
import com.alexstyl.specialdates.upcoming.widget.list.UpcomingEventsRemoteViewService;
import com.alexstyl.specialdates.upcoming.widget.list.UpcomingEventsScrollingAppWidgetProvider;
import com.alexstyl.specialdates.upcoming.widget.list.WidgetRouterActivity;
import com.alexstyl.specialdates.upcoming.widget.today.TodayAppWidgetProvider;
import com.alexstyl.specialdates.upcoming.widget.today.UpcomingWidgetConfigureActivity;
import com.alexstyl.specialdates.wear.WearSyncService;
import javax.inject.Singleton;
import org.jetbrains.annotations.NotNull;
import dagger.Component;
@Singleton
@Component(modules = {
AndroidApplicationModule.class,
AnalyticsModule.class,
ContactActionsModule.class,
ResourcesModule.class,
ContactsModule.class,
DateModule.class,
ImageModule.class,
ViewModule.class,
NamedayModule.class,
UpcomingEventsModule.class,
NamedaysInADayModule.class,
DailyReminderModule.class,
DonateModule.class,
SearchModule.class,
PeopleEventsModule.class,
BankHolidaysModule.class,
AddEventModule.class,
FacebookModule.class,
PeopleModule.class,
BankHolidaysModule.class,
ThemingModule.class,
RecentUpcomingPeopleEventsModule.class,
PersonModule.class
})
public interface AppComponent {
void inject(MementoApplication application);
void inject(HomeActivity activity);
void inject(UpcomingEventsFragment fragment);
void inject(AddEventActivity activity);
void inject(SearchActivity activity);
void inject(FacebookProfileActivity activity);
void inject(FacebookLogInActivity activity);
void inject(RateDialog activity);
void inject(UserSettingsFragment fragment);
void inject(DailyReminderFragment fragment);
void inject(DonateActivity activity);
void inject(UpcomingEventsScrollingAppWidgetProvider widgetProvider);
void inject(TodayAppWidgetProvider widgetProvider);
void inject(PersonActivity activity);
void inject(NamedaysOnADayActivity activity);
void inject(EventDatePickerDialogFragment fragment);
void inject(UpcomingEventsRemoteViewService viewService);
void inject(ContactSuggestionView view);
void inject(NamedayListPreference preference);
void inject(WearSyncService service);
void inject(ColorImageView view);
void inject(UpcomingWidgetConfigureActivity activity);
void inject(WidgetRouterActivity widgetRouterActivity);
void inject(DeviceConfigurationUpdatedReceiver receiver);
void inject(FacebookFriendsIntentService service);
void inject(ContactPermissionActivity activity);
void inject(BootCompleteReceiver receiver);
void inject(PeopleFragment peopleFragment);
void inject(FacebookWebView peopleFragment);
void inject(@NotNull ThemedMementoActivity themedMementoActivity);
void inject(@NotNull PersonActionsActivity personActionsActivity);
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/DeviceConfigurationUpdatedReceiver.java
================================================
package com.alexstyl.specialdates;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.alexstyl.specialdates.events.peopleevents.UpcomingEventsViewRefresher;
import javax.inject.Inject;
/**
* A {@linkplain BroadcastReceiver} that keeps track whether the user has updated some option on their device
* external to Memento which can affect the app.
*/
public class DeviceConfigurationUpdatedReceiver extends BroadcastReceiver {
@Inject UpcomingEventsViewRefresher viewRefresher;
@Inject CrashAndErrorTracker tracker;
@Override
public void onReceive(Context context, Intent intent) {
((MementoApplication) context.getApplicationContext()).getApplicationModule().inject(this);
String action = intent.getAction();
if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
tracker.updateLocaleUsed();
viewRefresher.refreshViews();
} else if (Intent.ACTION_DATE_CHANGED.equals(action)) {
viewRefresher.refreshViews();
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/EasyPreferences.java
================================================
package com.alexstyl.specialdates;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.support.annotation.BoolRes;
import android.support.annotation.StringRes;
import android.support.v4.util.Pair;
public final class EasyPreferences {
private final SharedPreferences prefs;
private final Resources res;
public static EasyPreferences createForDefaultPreferences(Context context) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
Resources resources = context.getResources();
return new EasyPreferences(preferences, resources);
}
public static EasyPreferences createForPrivatePreferences(Context context, @StringRes int fileName) {
SharedPreferences preferences = context.getSharedPreferences(context.getString(fileName), Context.MODE_PRIVATE);
Resources resources = context.getResources();
return new EasyPreferences(preferences, resources);
}
private EasyPreferences(SharedPreferences preferences, Resources resources) {
this.prefs = preferences;
this.res = resources;
}
private String key(int key) {
return res.getString(key);
}
public boolean getBoolean(@StringRes int bool, boolean defValue) {
return prefs.getBoolean(key(bool), defValue);
}
public boolean getBoolean(@StringRes int bool, @BoolRes int fallbackDefaultValue) {
boolean contains = prefs.contains(key(bool));
if (contains) {
return prefs.getBoolean(key(bool), false);
}
return res.getBoolean(fallbackDefaultValue);
}
public void setBoolean(@StringRes int key, boolean value) {
prefs.edit().putBoolean(key(key), value).apply();
}
public void setString(@StringRes int key, String value) {
prefs.edit().putString(key(key), value).apply();
}
public void setInteger(@StringRes int key, int value) {
prefs.edit().putInt(key(key), value).apply();
}
public int getInt(@StringRes int key, int defValue) {
return prefs.getInt(key(key), defValue);
}
public long getLong(@StringRes int key, long defValue) {
return prefs.getLong(key(key), defValue);
}
public String getString(@StringRes int key, String defValue) {
return prefs.getString(key(key), defValue);
}
public float getFloat(@StringRes int key, float defValue) {
return prefs.getFloat(key(key), defValue);
}
public void setLong(@StringRes int key, long value) {
prefs.edit().putLong(key(key), value).apply();
}
public void clear() {
prefs.edit().clear().apply();
}
public void setFloat(@StringRes int key, float value) {
prefs.edit().putFloat(key(key), value).apply();
}
public void setIntegers(Pair firstPair, Pair... otherPairs) {
SharedPreferences.Editor edit = prefs.edit();
edit.putInt(key(firstPair.first), firstPair.second);
for (Pair pair : otherPairs) {
edit.putInt(key(pair.first), pair.second);
}
edit.apply();
}
public void addOnPreferenceChangedListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
prefs.registerOnSharedPreferenceChangeListener(listener);
}
public void removeOnPreferenceChagnedListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
prefs.unregisterOnSharedPreferenceChangeListener(listener);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/ExternalNavigator.java
================================================
package com.alexstyl.specialdates;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
import android.widget.Toast;
import com.alexstyl.specialdates.analytics.Analytics;
import com.alexstyl.specialdates.analytics.Screen;
import com.alexstyl.specialdates.contact.Contact;
import com.alexstyl.specialdates.contact.ContactSource;
import com.alexstyl.specialdates.person.BottomSheetIntentDialog;
import com.alexstyl.specialdates.ui.base.MementoActivity;
import com.novoda.simplechromecustomtabs.SimpleChromeCustomTabs;
import java.util.ArrayList;
import java.util.List;
public class ExternalNavigator {
private final MementoActivity activity;
private final Analytics analytics;
private final CrashAndErrorTracker tracker;
public ExternalNavigator(MementoActivity activity, Analytics analytics, CrashAndErrorTracker tracker) {
this.activity = activity;
this.analytics = analytics;
this.tracker = tracker;
SimpleChromeCustomTabs.initialize(activity);
}
private Uri createPlayStoreUri() {
String packageName = activity.getPackageName();
return Uri.parse("market://details?id=" + packageName);
}
public void toPlayStore() {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, createPlayStoreUri());
activity.startActivity(intent);
analytics.trackScreen(Screen.PLAY_STORE);
} catch (ActivityNotFoundException e) {
tracker.track(e);
}
}
public void connectTo(Activity activity) {
SimpleChromeCustomTabs.getInstance().connectTo(activity);
}
public void disconnectTo(Activity activity) {
SimpleChromeCustomTabs.getInstance().disconnectFrom(activity);
}
public void toContactDetails(Contact contact) {
if (contact.getSource() == ContactSource.SOURCE_FACEBOOK) {
toFacebookContactDetails(contact);
} else if (contact.getSource() == ContactSource.SOURCE_DEVICE) {
toDeviceContactDetails(contact);
} else {
throw new IllegalStateException("Invalid contact source " + contact.getSource());
}
}
private void toFacebookContactDetails(Contact contact) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.facebook.com/" + contact.getContactID()));
activity.startActivity(intent);
} catch (ActivityNotFoundException ex) {
Toast.makeText(activity, R.string.no_app_found, Toast.LENGTH_SHORT).show();
}
}
private void toDeviceContactDetails(Contact contact) {
try {
ArrayList intents = viewContactIntentsFromOtherApps(contact);
if (intents.size() == 1) {
activity.startActivity(intents.get(0));
} else if (intents.size() > 1) {
// show bottom sheet with options
BottomSheetIntentDialog bottomSheetPicturesDialog =
BottomSheetIntentDialog.Companion.newIntent(
activity.getString(R.string.View_contact),
intents);
bottomSheetPicturesDialog.show(activity.getSupportFragmentManager(), "CONTACT");
}
} catch (ActivityNotFoundException ex) {
Toast.makeText(activity,
R.string.no_app_found,
Toast.LENGTH_SHORT).show();
}
}
private ArrayList viewContactIntentsFromOtherApps(Contact contact) {
PackageManager packageManager = activity.getPackageManager();
Intent intent = viewContact(contact);
List activities = packageManager.queryIntentActivities(intent, 0);
String myPackage = activity.getPackageName();
ArrayList targetIntents = new ArrayList<>();
for (ResolveInfo currentInfo : activities) {
String packageName = currentInfo.activityInfo.packageName;
if (!myPackage.equals(packageName)) {
Intent targetIntent = viewContact(contact);
intent.setPackage(packageName);
targetIntent.setClassName(packageName, currentInfo.activityInfo.name);
targetIntents.add(targetIntent);
}
}
return targetIntents;
}
private Intent viewContact(Contact contact) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.withAppendedPath(Contacts.CONTENT_URI, String.valueOf(contact.getContactID()));
intent.setData(uri);
return intent;
}
public void toFacebookPage() {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.facebook.com/memento.calendar/"));
activity.startActivity(intent);
} catch (ActivityNotFoundException ex) {
Toast.makeText(activity,
R.string.no_app_found,
Toast.LENGTH_SHORT)
.show();
}
analytics.trackScreen(Screen.FACEBOOK_PAGE);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/FabricTracker.java
================================================
package com.alexstyl.specialdates;
import android.content.Context;
import android.support.annotation.NonNull;
import com.alexstyl.specialdates.events.namedays.NamedayLocale;
import com.crashlytics.android.Crashlytics;
import com.novoda.notils.logger.simple.Log;
import java.util.Locale;
import io.fabric.sdk.android.Fabric;
public final class FabricTracker implements CrashAndErrorTracker {
private static final String TAG = FabricTracker.class.getSimpleName();
private static final String KEY_LOCALE = "user_locale";
private static final String KEY_NAMEDAY_LOCALE = "nameday_locale";
private static final String NONE = "no_locale";
private static boolean hasBeenInitialised = false;
private final Context context;
FabricTracker(Context context) {
this.context = context;
}
@Override
public void startTracking() {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Ignoring Crash Tracking in DEBUG builds");
return;
}
Fabric.with(context, new Crashlytics());
Log.i(TAG, "Crashlytics tracking started ");
hasBeenInitialised = true;
Crashlytics.setString(KEY_LOCALE, String.valueOf(Locale.getDefault()));
}
@Override
public void track(@NonNull Throwable e) {
if (hasBeenInitialised) {
Crashlytics.logException(e);
}
Log.w(e);
}
@Override
public void onNamedayLocaleChanged(NamedayLocale locale) {
if (hasBeenInitialised) {
Crashlytics.setString(KEY_NAMEDAY_LOCALE, locale == null ? NONE : locale.name());
}
}
@Override
public void updateLocaleUsed() {
if (hasBeenInitialised) {
Crashlytics.setString(KEY_LOCALE, String.valueOf(Locale.getDefault()));
}
}
@Override
public void log(String message) {
if (hasBeenInitialised) {
Crashlytics.log(message);
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/JobsCreator.kt
================================================
package com.alexstyl.specialdates
import com.alexstyl.specialdates.dailyreminder.DailyReminderJob
import com.alexstyl.specialdates.dailyreminder.DailyReminderNotifier
import com.alexstyl.specialdates.dailyreminder.DailyReminderPresenter
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsUpdater
import com.alexstyl.specialdates.upcoming.PeopleEventsRefreshJob
import com.evernote.android.job.Job
import com.evernote.android.job.JobCreator
class JobsCreator(private val peopleEventsUpdater: PeopleEventsUpdater,
private val presenter: DailyReminderPresenter,
private val notifier: DailyReminderNotifier) : JobCreator {
override fun create(tag: String): Job? =
when (tag) {
PeopleEventsRefreshJob.TAG -> PeopleEventsRefreshJob(peopleEventsUpdater)
DailyReminderJob.TAG -> DailyReminderJob(presenter, notifier)
else -> {
null
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/MementoApplication.java
================================================
package com.alexstyl.specialdates;
import android.app.AlarmManager;
import android.content.Context;
import android.support.multidex.MultiDexApplication;
import com.alexstyl.resources.ResourcesModule;
import com.alexstyl.specialdates.dailyreminder.DailyReminderScheduler;
import com.alexstyl.specialdates.dailyreminder.DailyReminderUserSettings;
import com.alexstyl.specialdates.events.namedays.activity.NamedaysInADayModule;
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsModule;
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsUpdater;
import com.alexstyl.specialdates.events.peopleevents.UpcomingEventsSettings;
import com.alexstyl.specialdates.facebook.FacebookModule;
import com.alexstyl.specialdates.facebook.FacebookUserSettings;
import com.alexstyl.specialdates.facebook.friendimport.FacebookFriendsScheduler;
import com.alexstyl.specialdates.images.AndroidContactsImageDownloader;
import com.alexstyl.specialdates.images.ImageModule;
import com.alexstyl.specialdates.images.NutraBaseImageDecoder;
import com.alexstyl.specialdates.permissions.MementoPermissions;
import com.alexstyl.specialdates.theming.ThemingModule;
import com.alexstyl.specialdates.ui.widget.ViewModule;
import com.alexstyl.specialdates.upcoming.PeopleEventsRefreshJob;
import com.evernote.android.job.DailyJob;
import com.evernote.android.job.JobManager;
import com.evernote.android.job.JobRequest;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.QueueProcessingType;
import com.nostra13.universalimageloader.utils.L;
import com.novoda.notils.logger.simple.Log;
import javax.inject.Inject;
import java.util.concurrent.TimeUnit;
import net.danlew.android.joda.JodaTimeAndroid;
public class MementoApplication extends MultiDexApplication {
private AppComponent appComponent;
@Inject CrashAndErrorTracker tracker;
@Inject FacebookUserSettings facebookSettings;
@Inject JobsCreator jobCreator;
@Inject PeopleEventsUpdater peopleEventsUpdater;
@Inject MementoPermissions permissions;
@Inject UpcomingEventsSettings settings;
@Inject DailyReminderUserSettings dailyReminderUserSettings;
@Inject DailyReminderScheduler androidDailyReminderScheduler;
@Override
public void onCreate() {
super.onCreate();
appComponent =
DaggerAppComponent.builder()
.androidApplicationModule(new AndroidApplicationModule(this))
.resourcesModule(new ResourcesModule(this, getResources()))
.imageModule(new ImageModule(getResources()))
.peopleEventsModule(new PeopleEventsModule(this))
.themingModule(new ThemingModule())
.viewModule(new ViewModule(getResources()))
.facebookModule(new FacebookModule(this))
.namedaysInADayModule(new NamedaysInADayModule())
.build();
appComponent.inject(this);
initialiseDependencies();
tracker.startTracking();
JobManager.create(this).addJobCreator(jobCreator);
if (dailyReminderUserSettings.isEnabled()) {
androidDailyReminderScheduler.scheduleReminderFor(dailyReminderUserSettings.getTimeSet());
}
if (facebookSettings.isLoggedIn()) {
AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
new FacebookFriendsScheduler(this, alarmManager).scheduleNext();// TODO use job schedulerAndroid
}
if (needsToInitialiseEvents()) {
peopleEventsUpdater
.updateEvents()
.subscribe();
}
schedulePeopleEventJob();
}
private boolean needsToInitialiseEvents() {
return permissions.canReadAndWriteContacts() && !settings.hasBeenInitialised();
}
private void schedulePeopleEventJob() {
DailyJob.schedule(
new JobRequest.Builder(PeopleEventsRefreshJob.TAG),
TimeUnit.HOURS.toMillis(1),
TimeUnit.HOURS.toMillis(3)
);
}
protected void initialiseDependencies() {
Log.setShowLogs(BuildConfig.DEBUG);
JodaTimeAndroid.init(this);
initImageLoader(this);
}
@SuppressWarnings("MagicNumber")
public static void initImageLoader(Context context) {
ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context)
.threadPriority(Thread.MIN_PRIORITY)
.threadPoolSize(10)
.tasksProcessingOrder(QueueProcessingType.LIFO)
.imageDecoder(new NutraBaseImageDecoder(BuildConfig.DEBUG))
.imageDownloader(new AndroidContactsImageDownloader(context));
L.writeLogs(BuildConfig.DEBUG);
com.nostra13.universalimageloader.core.ImageLoader.getInstance().init(config.build());
}
public AppComponent getApplicationModule() {
return appComponent;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/MementoConstants.kt
================================================
package com.alexstyl.specialdates
object MementoConstants {
var PACKAGE = BuildConfig.APPLICATION_ID
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/SQLArgumentBuilder.java
================================================
package com.alexstyl.specialdates;
import com.alexstyl.specialdates.date.Date;
public final class SQLArgumentBuilder {
private static final int TEN = 10;
private SQLArgumentBuilder() {
// hide this
}
public static String dateWithoutYear(Date date) {
StringBuilder stringBuilder = new StringBuilder();
int month = date.getMonth();
addWithLeadingZeroIfNeeded(stringBuilder, month);
stringBuilder.append("-");
addWithLeadingZeroIfNeeded(stringBuilder, date.getDayOfMonth());
return stringBuilder.toString();
}
private static void addWithLeadingZeroIfNeeded(StringBuilder stringBuilder, int value) {
if (value < TEN) {
stringBuilder.append("0");
}
stringBuilder.append(value);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/ShareAppIntentCreator.java
================================================
package com.alexstyl.specialdates;
import android.content.Intent;
public class ShareAppIntentCreator {
private static final String MARKET_LINK_SHORT = "http://goo.gl/ZQiAsi";
private final Strings strings;
public ShareAppIntentCreator(Strings strings) {
this.strings = strings;
}
public Intent buildIntent() {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, createShareText());
return intent;
}
private String createShareText() {
return String.format(strings.shareText(), strings.appName(), MARKET_LINK_SHORT);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/AccountData.java
================================================
package com.alexstyl.specialdates.addevent;
import android.graphics.drawable.Drawable;
public class AccountData {
static final AccountData NO_ACCOUNT = new AccountData(null, null, null);
private final String name;
private final String type;
private final Drawable icon;
AccountData(String name, String type, Drawable icon) {
this.name = name;
this.type = type;
this.icon = icon;
}
String getAccountName() {
return name;
}
String getAccountType() {
return type;
}
Drawable getIcon() {
return icon;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/AddEventActivity.kt
================================================
package com.alexstyl.specialdates.addevent
import android.Manifest
import android.annotation.TargetApi
import android.app.Activity
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.ContactsContract
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.MenuItem
import android.view.View
import com.alexstyl.android.toURI
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.MementoApplication
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.addevent.EventDatePickerDialogFragment.OnEventDatePickedListener
import com.alexstyl.specialdates.addevent.bottomsheet.BottomSheetPicturesDialog
import com.alexstyl.specialdates.addevent.bottomsheet.BottomSheetPicturesDialog.Listener
import com.alexstyl.specialdates.addevent.bottomsheet.PhotoPickerViewModel
import com.alexstyl.specialdates.addevent.ui.AvatarPickerView
import com.alexstyl.specialdates.analytics.Analytics
import com.alexstyl.specialdates.analytics.Screen
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.date.Date
import com.alexstyl.specialdates.events.peopleevents.EventType
import com.alexstyl.specialdates.events.peopleevents.ShortDateLabelCreator
import com.alexstyl.specialdates.images.ImageLoader
import com.alexstyl.specialdates.permissions.MementoPermissions
import com.alexstyl.specialdates.ui.base.ThemedMementoActivity
import com.alexstyl.specialdates.ui.widget.MementoToolbar
import com.theartofdev.edmodo.cropper.CropImage
import com.theartofdev.edmodo.cropper.CropImageView
import java.net.URI
import javax.inject.Inject
class AddEventActivity : ThemedMementoActivity(), Listener, OnEventDatePickedListener, DiscardPromptDialog.Listener {
lateinit var presenter: AddEventsPresenter
@Inject set
lateinit var permissionChecker: MementoPermissions
@Inject set
lateinit var uriFilePathProvider: UriFilePathProvider
@Inject set
lateinit var analytics: Analytics
@Inject set
lateinit var imageLoader: ImageLoader
@Inject set
lateinit var tracker: CrashAndErrorTracker
@Inject set
lateinit var shortDateLabelCreator: ShortDateLabelCreator
@Inject set
lateinit var view: AddEventView
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
overridePendingTransition(R.anim.slide_in_from_below, R.anim.stay)
setContentView(R.layout.activity_add_event)
val applicationModule = (application as MementoApplication).applicationModule
applicationModule.inject(this)
analytics.trackScreen(Screen.ADD_EVENT)
val toolbar = findViewById(R.id.memento_toolbar)
setSupportActionBar(toolbar)
toolbar.displayNavigationIconAsClose()
val avatarView = findViewById(R.id.add_event_avatar)
val eventsView = findViewById(R.id.add_event_events)
eventsView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
eventsView.setHasFixedSize(true)
val adapter = ContactEventsAdapter(contactDetailsListener)
eventsView.adapter = adapter
avatarView.setOnClickListener {
if (avatarView.isDisplayingAvatar) {
if (permissionChecker.canReadExternalStorage()) {
BottomSheetPicturesDialog
.includeClearImageOption()
.show(supportFragmentManager, "picture_pick")
} else {
requestExternalStoragePermission()
}
} else {
if (permissionChecker.canReadExternalStorage()) {
BottomSheetPicturesDialog.newInstance()
.show(supportFragmentManager, "picture_pick")
} else {
requestExternalStoragePermission()
}
}
}
val saveButton = findViewById(R.id.add_event_save)
saveButton.setOnClickListener {
presenter.saveChanges()
finishActivitySuccessfully()
}
view = AndroidAddEventView(avatarView, adapter, imageLoader, createToolbarAnimator(toolbar), saveButton)
presenter.startPresentingInto(view)
}
@TargetApi(Build.VERSION_CODES.M)
private fun requestExternalStoragePermission() {
requestPermissions(
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE, // read any images from disk to use as avatar
Manifest.permission.WRITE_EXTERNAL_STORAGE // save to disk any picture taken by camera
),
CODE_PERMISSION_EXTERNAL_STORAGE)
}
private fun createToolbarAnimator(toolbar: MementoToolbar): ToolbarBackgroundAnimator {
return if (resources.getBoolean(R.bool.isLandscape)) {
ToolbarBackgroundStubAnimator()
} else {
ToolbarBackgroundFadingAnimator.setupOn(toolbar)
}
}
private val contactDetailsListener = object : ContactDetailsListener {
override fun onContactCleared() {
presenter.removeContact()
}
override fun onAddEventClicked(viewModel: AddEventContactEventViewModel) {
val eventType = viewModel.eventType
val initialDate = viewModel.date
val dialog = EventDatePickerDialogFragment.newInstance(eventType, initialDate, shortDateLabelCreator)
dialog.show(supportFragmentManager, "pick_event")
}
override fun onRemoveEventClicked(eventType: EventType) {
presenter.removeEvent(eventType)
}
override fun onContactSelected(contact: Contact) {
presenter.presentContact(contact)
}
override fun onNameModified(newName: String) {
presenter.removeContact()
presenter.presentName(newName)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == CODE_PERMISSION_EXTERNAL_STORAGE && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
findViewById(android.R.id.content).post(Runnable {
if (isFinishing) {
return@Runnable
}
if (presenter.isDisplayingAvatar()) {
BottomSheetPicturesDialog
.includeClearImageOption()
.show(supportFragmentManager, "picture_pick")
} else {
BottomSheetPicturesDialog
.newInstance()
.show(supportFragmentManager, "picture_pick")
}
})
}
}
var viewModel: PhotoPickerViewModel? = null
override fun onImagePickerOptionSelected(viewModel: PhotoPickerViewModel) {
this.viewModel = viewModel
startActivityForResult(viewModel.intent, getRequestCodeFor(viewModel.intent))
}
private fun getRequestCodeFor(intent: Intent): Int {
val action = intent.action
return when (action) {
ImageIntentFactory.ACTION_IMAGE_CAPTURE -> CODE_TAKE_PICTURE
ImageIntentFactory.ACTION_IMAGE_PICK -> CODE_PICK_A_FILE
else -> throw IllegalArgumentException("Don't know how to handle $action")
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == CODE_TAKE_PICTURE && resultCode == Activity.RESULT_OK) {
analytics.trackImageCaptured()
startCropIntent(viewModel!!.absolutePath)
} else if (requestCode == CODE_PICK_A_FILE && resultCode == Activity.RESULT_OK) {
analytics.trackExistingImagePicked()
val imageUri = BottomSheetPicturesDialog.getImagePickResultUri(data!!)
startCropIntent(imageUri)
} else if (requestCode == CODE_CROP_IMAGE) {
val result = CropImage.getActivityResult(data)
if (resultCode == Activity.RESULT_OK) {
analytics.trackAvatarSelected()
presenter.present(result.uri.toURI())
} else if (resultCode == Activity.RESULT_CANCELED && result != null) {
tracker.track(result.error)
}
}
}
private fun startCropIntent(imageToCrop: URI) {
val prefix = if (imageToCrop.scheme == null) {
"file://"
} else {
""
}
val size = queryCropSize(contentResolver)
CropImage.activity(Uri.parse(prefix + imageToCrop.toString()))
.setGuidelines(CropImageView.Guidelines.ON)
.setAspectRatio(1, 1)
.setRequestedSize(size, size)
.start(this)
}
override fun onDatePicked(eventType: EventType, date: Date) {
presenter.onEventDatePicked(eventType, date)
}
private fun queryCropSize(resolver: ContentResolver): Int {
resolver.query(
ContactsContract.DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
arrayOf(ContactsContract.DisplayPhoto.DISPLAY_MAX_DIM), null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) {
return cursor.getInt(0)
}
}
return MAX_RESOLUTION
}
override fun onClearAvatarSelected() {
view.clearAvatar()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
if (presenter.isHoldingModifiedData) {
promptToDiscardBeforeExiting()
} else {
cancelActivity()
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun promptToDiscardBeforeExiting() {
DiscardPromptDialog()
.show(supportFragmentManager, "discard_prompt")
}
private fun finishActivitySuccessfully() {
analytics.trackEventAddedSuccessfully()
setResult(Activity.RESULT_OK)
finish()
}
private fun cancelActivity() {
analytics.trackAddEventsCancelled()
setResult(Activity.RESULT_CANCELED)
navigateUpToParent()
}
override fun finish() {
super.finish()
overridePendingTransition(R.anim.stay, R.anim.slide_out_from_below)
}
override fun onBackPressed() {
if (presenter.isHoldingModifiedData) {
promptToDiscardBeforeExiting()
} else {
super.onBackPressed()
}
}
override fun onDestroy() {
super.onDestroy()
presenter.stopPresenting()
}
override fun onDiscardChangesSelected() {
cancelActivity()
}
companion object {
private const val CODE_TAKE_PICTURE = 404
private const val CODE_PICK_A_FILE = 405
private const val CODE_CROP_IMAGE = CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE
private const val CODE_PERMISSION_EXTERNAL_STORAGE = 406
private const val MAX_RESOLUTION = 720
fun buildIntent(context: Context): Intent {
return Intent(context, AddEventActivity::class.java)
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/AddEventModule.kt
================================================
package com.alexstyl.specialdates.addevent
import android.content.ContentResolver
import android.content.Context
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.Strings
import com.alexstyl.specialdates.analytics.Analytics
import com.alexstyl.specialdates.date.DateLabelCreator
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsProvider
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsUpdater
import com.alexstyl.specialdates.events.peopleevents.ShortDateLabelCreator
import com.alexstyl.specialdates.images.ImageDecoder
import com.alexstyl.specialdates.images.ImageLoader
import dagger.Module
import dagger.Provides
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
@Module
class AddEventModule {
@Provides
fun presenter(analytics: Analytics,
contactOperations: ContactOperations,
messageDisplayer: MessageDisplayer,
operationsExecutorAndroid: ContactOperationsExecutor,
strings: Strings,
peopleEventsProvider: PeopleEventsProvider,
peopleUpdater: PeopleEventsUpdater,
factory: AddEventViewModelFactory) = AddEventsPresenter(
analytics,
contactOperations,
messageDisplayer,
operationsExecutorAndroid,
strings,
peopleEventsProvider,
factory,
peopleUpdater,
Schedulers.io(),
AndroidSchedulers.mainThread()
)
@Provides
fun factory(dateLabelCreator: DateLabelCreator, strings: Strings) = AddEventViewModelFactory(
dateLabelCreator, strings, AndroidEventIcons)
@Provides
fun messageDisplayer(context: Context): MessageDisplayer = ToastDisplayer(context)
@Provides
fun accountsProvider(context: Context) = WriteableAccountsProvider.from(context)
@Provides
fun operations() = ContactOperations()
@Provides
fun operationsExectutor(contentResolver: ContentResolver,
tracker: CrashAndErrorTracker,
peopleEventsProvider: PeopleEventsProvider,
accountsProvider: WriteableAccountsProvider,
imageDecoder: ImageDecoder): ContactOperationsExecutor {
return AndroidContactOperationsExecutor(contentResolver,
tracker,
ShortDateLabelCreator(),
peopleEventsProvider,
accountsProvider,
imageDecoder)
}
@Provides
fun filePathProvider(context: Context) = UriFilePathProvider(context)
@Provides
fun imageDecoder(imageLoader: ImageLoader): ImageDecoder {
return ImageDecoder(imageLoader)
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/AndroidAddEventView.kt
================================================
package com.alexstyl.specialdates.addevent
import android.graphics.Bitmap
import android.view.View
import com.alexstyl.specialdates.Optional
import com.alexstyl.specialdates.addevent.ui.AvatarPickerView
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.images.ImageLoadedConsumer
import com.alexstyl.specialdates.images.ImageLoader
import com.novoda.notils.meta.AndroidUtils
import java.net.URI
class AndroidAddEventView(private val avatarView: AvatarPickerView,
private val eventsAdapter: ContactEventsAdapter,
private val imageLoader: ImageLoader,
private val toolbarAnimator: ToolbarBackgroundAnimator,
private val saveButton: View) : AddEventView {
override fun allowImagePick() {
avatarView.isEnabled = true
}
override fun preventImagePick() {
avatarView.isEnabled = false
}
override fun allowSave() {
saveButton.isEnabled = true
}
override fun preventSave() {
saveButton.isEnabled = false
}
private var currentImageLoaded = Optional.absent()
override fun displayContact(contact: Contact) {
display(contact.imagePath)
AndroidUtils.requestHideKeyboard(avatarView.context, avatarView)
}
override fun display(viewModels: List) {
eventsAdapter.display(viewModels)
}
override fun display(uri: URI) {
imageLoader
.load(uri)
.withSize(avatarView.width, avatarView.height)
.into(object : ImageLoadedConsumer {
override fun onImageLoaded(loadedImage: Bitmap?) {
avatarView.setImageBitmap(loadedImage)
toolbarAnimator.fadeOut()
}
override fun onLoadingFailed() {
clearAvatar()
}
})
}
override fun clearAvatar() {
avatarView.setImageBitmap(null)
currentImageLoaded = Optional.absent()
toolbarAnimator.fadeIn()
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/AndroidContactOperationsExecutor.kt
================================================
package com.alexstyl.specialdates.addevent
import android.content.ContentProviderOperation
import android.content.ContentResolver
import android.content.OperationApplicationException
import android.database.Cursor
import android.os.RemoteException
import android.provider.ContactsContract
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.addevent.operations.ContactOperation
import com.alexstyl.specialdates.addevent.operations.InsertContact
import com.alexstyl.specialdates.addevent.operations.UpdateContact
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsProvider
import com.alexstyl.specialdates.events.peopleevents.ShortDateLabelCreator
import com.alexstyl.specialdates.images.ImageDecoder
import com.novoda.notils.exception.DeveloperError
import java.util.ArrayList
class AndroidContactOperationsExecutor(
private val contentResolver: ContentResolver,
private val tracker: CrashAndErrorTracker,
private val displayStringCreator: ShortDateLabelCreator,
private val peopleEventsProvider: PeopleEventsProvider,
private val accountsProvider: WriteableAccountsProvider,
private val imageDecoder: ImageDecoder)
: ContactOperationsExecutor {
override fun execute(operations: List): Boolean {
val operationsFactory = makeFactoryFor(operations[0])
try {
val contentProviderOperations = ArrayList(operations.fold(emptyList(), { list, contactOperation ->
list + operationsFactory.createOperationsFor(contactOperation)
}))
contentResolver.applyBatch(ContactsContract.AUTHORITY, contentProviderOperations)
return true
} catch (e: RemoteException) {
tracker.track(e)
} catch (e: OperationApplicationException) {
tracker.track(e)
}
return false
}
private fun makeFactoryFor(contactOperation: ContactOperation): OperationsFactory {
if (contactOperation is InsertContact) {
return OperationsFactory.forNewContact(displayStringCreator, peopleEventsProvider, accountsProvider, imageDecoder)
} else if (contactOperation is UpdateContact) {
val rawContactID = rawContactID(contactOperation.contact)
return OperationsFactory(rawContactID, displayStringCreator, peopleEventsProvider, accountsProvider, imageDecoder)
}
throw IllegalArgumentException("Cannot make factory for $contactOperation")
}
private fun rawContactID(contact: Contact): Int {
val projection = arrayOf(ContactsContract.CommonDataKinds.Event.RAW_CONTACT_ID)
val selection = ContactsContract.CommonDataKinds.Event.CONTACT_ID + " = ?"
val selectionArgs = arrayOf(contact.contactID.toString())
val cursor = contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, selectionArgs, null)
throwIfInvalid(cursor)
try {
if (cursor!!.moveToFirst()) {
return cursor.getInt(0)
}
} finally {
cursor!!.close()
}
return 0
}
private fun throwIfInvalid(cursor: Cursor?) {
if (cursor == null || cursor.isClosed) {
throw DeveloperError("Cursor was invalid")
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/AndroidEventIcons.kt
================================================
package com.alexstyl.specialdates.addevent
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.events.database.EventTypeId
import com.alexstyl.specialdates.events.peopleevents.EventType
object AndroidEventIcons : EventIcons {
override fun iconOf(eventType: EventType): Int = when (eventType.id) {
EventTypeId.TYPE_BIRTHDAY -> R.drawable.ic_cake
EventTypeId.TYPE_NAMEDAY -> R.drawable.ic_face
EventTypeId.TYPE_ANNIVERSARY -> R.drawable.ic_anniversary
EventTypeId.TYPE_OTHER -> R.drawable.ic_other
EventTypeId.TYPE_CUSTOM -> R.drawable.ic_custom
else -> {
throw IllegalStateException("No icon for type $eventType")
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ContactDetailsListener.java
================================================
package com.alexstyl.specialdates.addevent;
import com.alexstyl.specialdates.contact.Contact;
import com.alexstyl.specialdates.events.peopleevents.EventType;
interface ContactDetailsListener {
void onAddEventClicked(AddEventContactEventViewModel viewModel);
void onRemoveEventClicked(EventType eventType);
void onContactSelected(Contact contact);
void onNameModified(String newName);
void onContactCleared();
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ContactEventViewHolder.java
================================================
package com.alexstyl.specialdates.addevent;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
final class ContactEventViewHolder extends RecyclerView.ViewHolder {
private final ImageView icon;
private final TextView datePicker;
private final ImageButton removeEvent;
ContactEventViewHolder(View view, ImageView icon, TextView datePicker, ImageButton removeEvent) {
super(view);
this.icon = icon;
this.datePicker = datePicker;
this.removeEvent = removeEvent;
}
public void bind(final AddEventContactEventViewModel viewModel, final ContactDetailsListener contactDetailsListener) {
icon.setImageResource(viewModel.getEventIconRes());
datePicker.setHint(viewModel.getHintText());
datePicker.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
contactDetailsListener.onAddEventClicked(viewModel);
}
});
boolean clearVisibility = viewModel.getClearVisibility();
removeEvent.setVisibility(clearVisibility ? View.VISIBLE : View.GONE);
removeEvent.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
contactDetailsListener.onRemoveEventClicked(viewModel.getEventType());
}
});
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ContactEventsAdapter.java
================================================
package com.alexstyl.specialdates.addevent;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.addevent.ui.ContactSuggestionView;
import com.novoda.notils.caster.Views;
import java.util.ArrayList;
import java.util.List;
public final class ContactEventsAdapter extends RecyclerView.Adapter {
private static final int HEADER_COUNT = 1;
private static final int TYPE_CONTACT_SUGGESTION = 0;
private static final int TYPE_EVENT = 1;
private final List viewModels = new ArrayList<>();
private final ContactDetailsListener contactDetailsListener;
ContactEventsAdapter(ContactDetailsListener contactDetailsListener) {
this.contactDetailsListener = contactDetailsListener;
}
@Override
public int getItemViewType(int position) {
return position == 0 ? TYPE_CONTACT_SUGGESTION : TYPE_EVENT;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_CONTACT_SUGGESTION) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view = layoutInflater.inflate(R.layout.row_add_event_contact_suggestion, parent, false);
ContactSuggestionView suggestionView = Views.findById(view, R.id.add_event_contact_autocomplete);
return new ContactSuggestionViewHolder(suggestionView);
} else if (viewType == TYPE_EVENT) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view = layoutInflater.inflate(R.layout.row_add_event_contact_event, parent, false);
ImageView icon = Views.findById(view, R.id.add_event_event_icon);
TextView datePicker = Views.findById(view, R.id.add_event_date_picker);
ImageButton clear = Views.findById(view, R.id.add_event_remove_event);
return new ContactEventViewHolder(view, icon, datePicker, clear);
} else {
throw new IllegalStateException("Received viewType " + viewType);
}
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
int viewType = getItemViewType(position);
if (viewType == TYPE_CONTACT_SUGGESTION) {
((ContactSuggestionViewHolder) holder).bind(contactDetailsListener);
} else if (viewType == TYPE_EVENT) {
AddEventContactEventViewModel addEventContactEventViewModel = viewModels.get(position - HEADER_COUNT);
((ContactEventViewHolder) holder).bind(addEventContactEventViewModel, contactDetailsListener);
} else {
throw new IllegalStateException("Unable to bind view type " + viewType);
}
}
@Override
public int getItemCount() {
return viewModels.size() + HEADER_COUNT;
}
void display(List viewModels) {
this.viewModels.clear();
this.viewModels.addAll(viewModels);
notifyItemRangeChanged(1, getItemCount() - 1);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ContactSuggestionViewHolder.java
================================================
package com.alexstyl.specialdates.addevent;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import com.alexstyl.specialdates.addevent.ui.ContactSuggestionView;
import com.alexstyl.specialdates.contact.Contact;
import com.novoda.notils.text.SimpleTextWatcher;
final class ContactSuggestionViewHolder extends RecyclerView.ViewHolder {
private ContactSuggestionView contactSuggestionView;
ContactSuggestionViewHolder(ContactSuggestionView contactSuggestionView) {
super(contactSuggestionView);
this.contactSuggestionView = contactSuggestionView;
}
public void bind(final ContactDetailsListener listener) {
contactSuggestionView.setOnContactSelectedListener(new ContactSuggestionView.OnContactSelectedListener() {
@Override
public void onContactSelected(Contact contact) {
listener.onContactSelected(contact);
}
@Override
public void onContactCleared() {
listener.onContactCleared();
}
});
contactSuggestionView.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void afterTextChanged(Editable text) {
listener.onNameModified(text.toString());
}
});
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/DiscardPromptDialog.java
================================================
package com.alexstyl.specialdates.addevent;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.ui.base.MementoDialog;
import com.novoda.notils.caster.Classes;
public class DiscardPromptDialog extends MementoDialog {
interface Listener {
void onDiscardChangesSelected();
}
private Listener listener;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
listener = Classes.from(activity);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.add_event_discard_changes_title)
.setPositiveButton(R.string.add_event_discard_changes_accept, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
listener.onDiscardChangesSelected();
}
})
.setNegativeButton(android.R.string.no, null)
.create();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/EventDatePickerDialogFragment.java
================================================
package com.alexstyl.specialdates.addevent;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import com.alexstyl.specialdates.Strings;
import com.alexstyl.specialdates.AppComponent;
import com.alexstyl.specialdates.Optional;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.addevent.ui.EventDatePicker;
import com.alexstyl.specialdates.date.Date;
import com.alexstyl.specialdates.date.DateParseException;
import com.alexstyl.specialdates.events.database.EventTypeId;
import com.alexstyl.specialdates.events.peopleevents.EventType;
import com.alexstyl.specialdates.events.peopleevents.ShortDateLabelCreator;
import com.alexstyl.specialdates.events.peopleevents.StandardEventType;
import com.alexstyl.specialdates.ui.base.MementoDialog;
import com.alexstyl.specialdates.date.DateParser;
import com.novoda.notils.caster.Classes;
import com.novoda.notils.caster.Views;
import javax.inject.Inject;
public class EventDatePickerDialogFragment extends MementoDialog {
private static final String KEY_DATE = "key:date";
private static final String ARG_EVENT_TYPE_ID = "arg:event_type_id";
private OnEventDatePickedListener listener;
private EventDatePicker datePicker;
private Optional initialDate;
@Inject Strings strings;
@Inject DateParser dateParser;
public static EventDatePickerDialogFragment newInstance(EventType eventType, Optional date, ShortDateLabelCreator shortDateLabelCreator) {
EventDatePickerDialogFragment dialogFragment = new EventDatePickerDialogFragment();
Bundle args = new Bundle(2);
args.putInt(ARG_EVENT_TYPE_ID, eventType.getId());
if (date.isPresent()) {
String label = shortDateLabelCreator.createLabelWithYearPreferredFor(date.get());
args.putString(KEY_DATE, label);
}
dialogFragment.setArguments(args);
return dialogFragment;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.listener = Classes.from(activity);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppComponent applicationModule = getApplication().getApplicationModule();
applicationModule.inject(this);
initialDate = getDate();
}
private Optional getDate() {
Bundle arguments = getArguments();
if (arguments.containsKey(KEY_DATE)) {
String birthday = arguments.getString(KEY_DATE);
return parseFrom(birthday);
} else {
return Optional.Companion.absent();
}
}
private Optional parseFrom(String birthday) {
try {
Date parsedDate = dateParser.parse(birthday);
return new Optional<>(parsedDate);
} catch (DateParseException e) {
e.printStackTrace();
return Optional.Companion.absent();
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View view = LayoutInflater.from(getThemedContext()).inflate(R.layout.dialog_birthday_picker, null, false);
datePicker = Views.findById(view, R.id.dialog_birthday_picker);
final EventType eventType = getEventType();
if (initialDate.isPresent()) {
datePicker.setDisplayingDate(initialDate.get());
}
return new AlertDialog.Builder(getActivity())
.setTitle(eventType.getEventName(strings))
.setView(view)
.setPositiveButton(R.string.birthday_picker_dialog_positive, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Date date = datePicker.getDisplayingDate();
listener.onDatePicked(eventType, date);
}
})
.create();
}
public EventType getEventType() {
@EventTypeId int eventTypeId = getArguments().getInt(ARG_EVENT_TYPE_ID);
return StandardEventType.fromId(eventTypeId);
}
public interface OnEventDatePickedListener {
void onDatePicked(EventType eventType, Date date);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ImageIntentFactory.java
================================================
package com.alexstyl.specialdates.addevent;
import android.content.Intent;
import android.net.Uri;
import android.provider.MediaStore;
public final class ImageIntentFactory {
static final String ACTION_IMAGE_PICK = Intent.ACTION_PICK;
static final String ACTION_IMAGE_CAPTURE = MediaStore.ACTION_IMAGE_CAPTURE;
public Intent pickExistingImage() {
Intent intent = new Intent(ACTION_IMAGE_PICK);
intent.setType("image/*");
intent.addFlags(
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION
);
return intent;
}
public Intent captureNewPhoto(Uri outputUri) {
Intent takePictureIntent = new Intent(ACTION_IMAGE_CAPTURE);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
return takePictureIntent;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/OnCameraClickedListener.java
================================================
package com.alexstyl.specialdates.addevent;
interface OnCameraClickedListener {
void onPictureRetakenRequested();
void onNewPictureTakenRequested();
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/OperationsFactory.kt
================================================
package com.alexstyl.specialdates.addevent
import android.content.ContentProviderOperation
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.Event
import android.provider.ContactsContract.CommonDataKinds.Photo
import android.provider.ContactsContract.CommonDataKinds.StructuredName
import android.provider.ContactsContract.Data
import com.alexstyl.specialdates.addevent.operations.ContactOperation
import com.alexstyl.specialdates.addevent.operations.InsertContact
import com.alexstyl.specialdates.addevent.operations.InsertEvent
import com.alexstyl.specialdates.addevent.operations.InsertImage
import com.alexstyl.specialdates.addevent.operations.UpdateContact
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.date.ContactEvent
import com.alexstyl.specialdates.date.Date
import com.alexstyl.specialdates.date.TimePeriod
import com.alexstyl.specialdates.events.database.EventTypeId
import com.alexstyl.specialdates.events.peopleevents.EventType
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsProvider
import com.alexstyl.specialdates.events.peopleevents.ShortDateLabelCreator
import com.alexstyl.specialdates.events.peopleevents.StandardEventType
import com.alexstyl.specialdates.images.ImageDecoder
import java.net.URI
class OperationsFactory(private val rawContactID: Int,
private val displayStringCreator: ShortDateLabelCreator,
private val peopleEventsProvider: PeopleEventsProvider,
private val accountsProvider: WriteableAccountsProvider,
private val imageDecoder: ImageDecoder) {
fun createOperationsFor(contactOperation: ContactOperation): List {
when (contactOperation) {
is InsertContact -> return createContactIn(accountToStoreContact, contactOperation.contactName)
is UpdateContact -> return updateExistingContact(contactOperation.contact)
is InsertEvent -> return newInsertFor(contactOperation.eventType, contactOperation.date)
is InsertImage -> return insertImageFor(contactOperation.imageUri)
}
throw IllegalArgumentException("Unable to create operation for $contactOperation")
}
private fun insertImageFor(imageUri: URI): List {
val decodeFrom = imageDecoder.decodeFrom(imageUri)
if (decodeFrom != null) {
val builder = ContentProviderOperation.newInsert(Data.CONTENT_URI)
.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE)
.withValue(Photo.PHOTO, decodeFrom.bytes)
addRawContactID(builder)
return listOf(builder.build())
} else {
return emptyList()
}
}
private fun newInsertFor(eventType: EventType, date: Date): List {
val builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI)
.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE)
.withValue(Event.TYPE, androidIdOf(eventType))
.withValue(Event.START_DATE, displayStringCreator.createLabelWithYearPreferredFor(date))
addRawContactID(builder)
return listOf(builder.build())
}
private fun androidIdOf(eventType: EventType): Int = when (eventType.id) {
EventTypeId.TYPE_BIRTHDAY -> Event.TYPE_BIRTHDAY
EventTypeId.TYPE_ANNIVERSARY -> Event.TYPE_ANNIVERSARY
EventTypeId.TYPE_CUSTOM -> Event.TYPE_CUSTOM
EventTypeId.TYPE_OTHER -> Event.TYPE_OTHER
else -> {
throw IllegalStateException("There is no Android type of $eventType")
}
}
private fun addRawContactID(builder: ContentProviderOperation.Builder) {
if (rawContactID == NO_RAW_CONTACT_ID) {
builder.withValueBackReference(Data.RAW_CONTACT_ID, rawContactID)
} else {
builder.withValue(Data.RAW_CONTACT_ID, rawContactID)
}
}
private fun deleteEvents(contactEvents: List): ArrayList {
val ops = ArrayList()
for (contactEvent in contactEvents) {
val eventId = contactEvent.deviceEventId.get()
ops.add(
ContentProviderOperation
.newDelete(Data.CONTENT_URI)
.withSelection(Event._ID + "= " + eventId, null)
.build())
}
return ops
}
private fun createContactIn(account: AccountData, contactName: String): ArrayList {
val ops = ArrayList(2)
ops.add(
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, account.accountName)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, account.accountType)
.build())
ops.add(
ContentProviderOperation.newInsert(Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, rawContactID)
.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
.withValue(StructuredName.DISPLAY_NAME, contactName)
.build())
return ops
}
private val accountToStoreContact: AccountData
get() {
val availableAccounts = accountsProvider.availableAccounts
return if (availableAccounts.size == 0) {
AccountData.NO_ACCOUNT
} else {
availableAccounts[0]
}
}
private fun updateExistingContact(contact: Contact): List {
val contactEvents = getAllDeviceEventsFor(contact)
return deleteEvents(contactEvents)
}
private fun getAllDeviceEventsFor(contact: Contact): List {
val contactEvents = ArrayList()
val contactEventsOnDate = peopleEventsProvider.fetchEventsBetween(TimePeriod.aYearFromNow())
for (contactEvent in contactEventsOnDate) {
val (contactID) = contactEvent.contact
if (contactID == contact.contactID && contactEvent.type !== StandardEventType.NAMEDAY) {
contactEvents.add(contactEvent)
}
}
return contactEvents
}
companion object {
private const val NO_RAW_CONTACT_ID = 0
fun forNewContact(displayStringCreator: ShortDateLabelCreator,
peopleEventsProvider: PeopleEventsProvider,
accountsProvider: WriteableAccountsProvider,
imageDecoder: ImageDecoder): OperationsFactory {
return OperationsFactory(NO_RAW_CONTACT_ID, displayStringCreator, peopleEventsProvider, accountsProvider,
imageDecoder)
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ToastDisplayer.kt
================================================
package com.alexstyl.specialdates.addevent
import android.content.Context
import android.widget.Toast
class ToastDisplayer(private val context: Context) : MessageDisplayer {
override fun showMessage(string: String) {
Toast.makeText(context, string, Toast.LENGTH_SHORT).show()
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ToolbarBackgroundAnimator.java
================================================
package com.alexstyl.specialdates.addevent;
public interface ToolbarBackgroundAnimator {
void fadeOut();
void fadeIn();
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ToolbarBackgroundFadingAnimator.java
================================================
package com.alexstyl.specialdates.addevent;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.support.v7.widget.Toolbar;
final class ToolbarBackgroundFadingAnimator implements ToolbarBackgroundAnimator {
private static final int FADING_DURATION = 400;
private TransitionDrawable transitionDrawable;
private boolean visible = true;
static ToolbarBackgroundFadingAnimator setupOn(Toolbar toolbar) {
ColorDrawable colorDrawable = new ColorDrawable(toolbar.getResources().getColor(android.R.color.transparent));
TransitionDrawable transitionDrawable = new TransitionDrawable(layersFrom(toolbar.getBackground(), colorDrawable));
transitionDrawable.setCrossFadeEnabled(true);
toolbar.setBackground(transitionDrawable);
return new ToolbarBackgroundFadingAnimator(transitionDrawable);
}
private ToolbarBackgroundFadingAnimator(TransitionDrawable transitionDrawable) {
this.transitionDrawable = transitionDrawable;
}
@Override
public void fadeOut() {
if (visible) {
transitionDrawable.startTransition(FADING_DURATION);
visible = false;
}
}
private static Drawable[] layersFrom(Drawable from, Drawable to) {
Drawable[] layers = new Drawable[2];
layers[0] = from;
layers[1] = to;
return layers;
}
@Override
public void fadeIn() {
if (!visible) {
transitionDrawable.reverseTransition(FADING_DURATION);
visible = true;
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ToolbarBackgroundStubAnimator.java
================================================
package com.alexstyl.specialdates.addevent;
final class ToolbarBackgroundStubAnimator implements ToolbarBackgroundAnimator {
@Override
public void fadeOut() {
// does nothing
}
@Override
public void fadeIn() {
// does nothing
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/UriFilePathProvider.kt
================================================
package com.alexstyl.specialdates.addevent
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.support.v4.content.FileProvider
import com.alexstyl.specialdates.BuildConfig
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
class UriFilePathProvider(private val context: Context) {
fun uriFor(file: File): Uri {
return FileProvider.getUriForFile(
context,
BuildConfig.FILE_PROVIDER,
file
)
}
@Throws(IOException::class)
fun createImageFile(): File {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val image = File.createTempFile(
imageFileName,
".jpg",
storageDir
)
return image
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/WriteableAccountsProvider.java
================================================
package com.alexstyl.specialdates.addevent;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import java.util.ArrayList;
public final class WriteableAccountsProvider {
private static final String GOOGLE_ACCOUNT = "com.google";
private final AccountManager accountManager;
private final PackageManager packageManager;
public static WriteableAccountsProvider from(Context context) {
AccountManager manager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
PackageManager packageManager = context.getPackageManager();
return new WriteableAccountsProvider(manager, packageManager);
}
private WriteableAccountsProvider(AccountManager accountManager, PackageManager packageManager) {
this.accountManager = accountManager;
this.packageManager = packageManager;
}
ArrayList getAvailableAccounts() {
ArrayList accounts = new ArrayList<>();
AuthenticatorDescription[] accountTypes = accountManager.getAuthenticatorTypes();
for (Account account : accountManager.getAccounts()) {
String accountType = account.type;
if (accountIsWritable(accountType)) {
AuthenticatorDescription description = getAuthenticatorDescription(accountType, accountTypes);
Drawable icon = packageManager.getDrawable(description.packageName, description.iconId, null);
accounts.add(new AccountData(account.name, description.type, icon));
}
}
return accounts;
}
private static boolean accountIsWritable(String accountType) {
return GOOGLE_ACCOUNT.equals(accountType);
}
private static AuthenticatorDescription getAuthenticatorDescription(String type,
AuthenticatorDescription[] dictionary) {
for (int i = 0; i < dictionary.length; i++) {
if (dictionary[i].type.equals(type)) {
return dictionary[i];
}
}
throw new RuntimeException("Unable to find matching authenticator");
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/bottomsheet/BottomSheetPicturesDialog.kt
================================================
package com.alexstyl.specialdates.addevent.bottomsheet
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.BottomSheetDialog
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.addevent.ImageIntentFactory
import com.alexstyl.specialdates.addevent.UriFilePathProvider
import com.alexstyl.specialdates.ui.base.MementoDialog
import com.alexstyl.specialdates.ui.widget.SpacesItemDecoration
import com.novoda.notils.caster.Classes
import com.novoda.notils.caster.Views
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.net.URI
class BottomSheetPicturesDialog : MementoDialog() {
private var parentListener: Listener? = null
private lateinit var adapter: ImagePickerOptionsAdapter
private lateinit var photoPickerViewModelFactory: PhotoPickerViewModelFactory
private var disposable: Disposable? = null
private val includeClear: Boolean
get() = arguments != null && arguments!!.getBoolean(KEY_INCLUDE_CLEAR, false)
private val internalListener = object : Listener {
override fun onImagePickerOptionSelected(viewModel: PhotoPickerViewModel) {
dismiss()
parentListener?.onImagePickerOptionSelected(viewModel)
}
override fun onClearAvatarSelected() {
dismiss()
parentListener?.onClearAvatarSelected()
}
}
override fun onAttach(activity: Activity) {
super.onAttach(activity)
parentListener = Classes.from(activity)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
photoPickerViewModelFactory = PhotoPickerViewModelFactory(
UriFilePathProvider(activity!!),
IntentResolver(activity!!.packageManager),
ImageIntentFactory(),
activity!!.packageManager
)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = BottomSheetDialog(activity!!)
val layoutInflater = LayoutInflater.from(activity!!)
val view = layoutInflater.inflate(R.layout.dialog_pick_image, null, false)
val grid = Views.findById(view, R.id.pick_image_grid)
val resources = resources
val gridLayoutManager = GridLayoutManager(activity!!, resources.getInteger(R.integer.bottom_sheet_span_count))
grid.addItemDecoration(SpacesItemDecoration(
resources.getDimensionPixelSize(R.dimen.add_event_image_option_vertical),
gridLayoutManager.spanCount
))
grid.layoutManager = gridLayoutManager
adapter = if (includeClear)
ImagePickerOptionsAdapter.createWithClear(internalListener)
else
ImagePickerOptionsAdapter.newInstance(internalListener)
grid.adapter = adapter
dialog.setContentView(view)
return dialog
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
disposable = Observable.fromCallable {
photoPickerViewModelFactory.createViewModels()
}.doOnError {
it.printStackTrace()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { viewModels -> adapter.updateWith(viewModels) }
}
override fun onDestroy() {
super.onDestroy()
disposable?.dispose()
}
interface Listener {
/**
* Called when the user selects the option to select an picture as an avatar, via the [BottomSheetPicturesDialog]
*
*/
fun onImagePickerOptionSelected(viewModel: PhotoPickerViewModel)
/**
* Called when the user selects the option to clear the existing avatar, via the [BottomSheetPicturesDialog]
*/
fun onClearAvatarSelected()
}
companion object {
private const val KEY_INCLUDE_CLEAR = "key_include_clear"
fun newInstance(): BottomSheetPicturesDialog {
return BottomSheetPicturesDialog()
}
fun includeClearImageOption(): BottomSheetPicturesDialog {
val args = Bundle(1)
args.putBoolean(KEY_INCLUDE_CLEAR, true)
val fragment = BottomSheetPicturesDialog()
fragment.arguments = args
return fragment
}
fun getImagePickResultUri(data: Intent): URI {
return URI.create(data.data.toString())
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/bottomsheet/ClearImageViewHolder.java
================================================
package com.alexstyl.specialdates.addevent.bottomsheet;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.alexstyl.specialdates.R;
final class ClearImageViewHolder extends RecyclerView.ViewHolder {
private final ImageView imageView;
private final TextView textView;
ClearImageViewHolder(View itemView, ImageView imageView, TextView textView) {
super(itemView);
this.imageView = imageView;
this.textView = textView;
}
void bind(final BottomSheetPicturesDialog.Listener listener) {
imageView.setImageResource(R.drawable.ic_clear);
textView.setText(R.string.add_event_remove_photo);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onClearAvatarSelected();
}
});
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/bottomsheet/ImagePickerOptionViewHolder.kt
================================================
package com.alexstyl.specialdates.addevent.bottomsheet
import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.alexstyl.specialdates.addevent.bottomsheet.BottomSheetPicturesDialog.Listener
class ImagePickerOptionViewHolder(view: View,
private val iconView: ImageView,
private val labelView: TextView
) : RecyclerView.ViewHolder(view) {
fun bind(viewModel: PhotoPickerViewModel, listener: Listener) {
iconView.setImageDrawable(viewModel.activityIcon)
labelView.text = viewModel.label
itemView.setOnClickListener { listener.onImagePickerOptionSelected(viewModel) }
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/bottomsheet/ImagePickerOptionsAdapter.java
================================================
package com.alexstyl.specialdates.addevent.bottomsheet;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.addevent.bottomsheet.BottomSheetPicturesDialog.Listener;
import java.util.ArrayList;
import java.util.List;
final class ImagePickerOptionsAdapter extends RecyclerView.Adapter {
private static final int TYPE_CLEAR = 0;
private static final int TYPE_OPTION = 1;
private final List viewModels;
private final Listener listener;
private boolean includeClear;
private final int includeClearCount;
public static ImagePickerOptionsAdapter newInstance(Listener listener) {
return new ImagePickerOptionsAdapter(listener, false);
}
static ImagePickerOptionsAdapter createWithClear(Listener listener) {
return new ImagePickerOptionsAdapter(listener, true);
}
private ImagePickerOptionsAdapter(Listener listener, boolean includeClear) {
this.includeClear = includeClear;
this.viewModels = new ArrayList<>();
this.listener = listener;
this.includeClearCount = includeClear ? 1 : 0;
}
@Override
public int getItemViewType(int position) {
if (includeClear && position == 0) {
return TYPE_CLEAR;
}
return TYPE_OPTION;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_image_option, parent, false);
ImageView iconView = view.findViewById(R.id.pick_image_activity_icon);
TextView labelView = view.findViewById(R.id.pick_image_activity_label);
if (viewType == TYPE_CLEAR) {
return new ClearImageViewHolder(view, iconView, labelView);
} else if (viewType == TYPE_OPTION) {
return new ImagePickerOptionViewHolder(view, iconView, labelView);
} else {
throw new IllegalStateException("Illegal view type " + viewType);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int itemViewType = getItemViewType(position);
if (itemViewType == TYPE_CLEAR) {
((ClearImageViewHolder) holder).bind(listener);
} else if (itemViewType == TYPE_OPTION) {
PhotoPickerViewModel viewModel = viewModels.get(position - includeClearCount);
((ImagePickerOptionViewHolder) holder).bind(viewModel, listener);
} else {
throw new IllegalStateException("Illegal view type " + itemViewType);
}
}
@Override
public int getItemCount() {
return viewModels.size() + includeClearCount;
}
void updateWith(List viewModels) {
this.viewModels.clear();
this.viewModels.addAll(viewModels);
notifyDataSetChanged();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/bottomsheet/IntentResolver.kt
================================================
package com.alexstyl.specialdates.addevent.bottomsheet
import android.content.Intent
import android.content.pm.PackageManager
import java.net.URI
class IntentResolver(private val packageManager: PackageManager) {
internal fun createViewModelsFor(intent: Intent, absolutePath: String): List {
return packageManager.queryIntentActivities(intent, 0)
.map { resolveInfo ->
val icon = resolveInfo.loadIcon(packageManager)
val label = resolveInfo.loadLabel(packageManager).toString()
val launchingIntent = Intent(intent)
launchingIntent.setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name)
PhotoPickerViewModel(icon, label, launchingIntent, URI.create(absolutePath))
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/bottomsheet/PhotoPickerViewModel.kt
================================================
package com.alexstyl.specialdates.addevent.bottomsheet
import android.content.Intent
import android.graphics.drawable.Drawable
import java.net.URI
data class PhotoPickerViewModel(
val activityIcon: Drawable,
val label: String,
val intent: Intent,
val absolutePath: URI)
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/bottomsheet/PhotoPickerViewModelFactory.kt
================================================
package com.alexstyl.specialdates.addevent.bottomsheet
import android.content.Intent
import android.content.pm.PackageManager
import android.provider.MediaStore
import com.alexstyl.specialdates.addevent.ImageIntentFactory
import com.alexstyl.specialdates.addevent.UriFilePathProvider
class PhotoPickerViewModelFactory(private val uriFilePathProvider: UriFilePathProvider,
private val intentResolver: IntentResolver,
private val intentCreator: ImageIntentFactory,
private val packageManager: PackageManager) {
fun createViewModels(): List {
return capturePhotoViewModels() + pickPhotoViewModels()
}
private fun capturePhotoViewModels(): List {
val file = uriFilePathProvider.createImageFile()
val outputUri = uriFilePathProvider.uriFor(file)
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
return if (takePictureIntent.resolveActivity(packageManager) != null) {
intentResolver.createViewModelsFor(takePictureIntent, file.absolutePath)
} else {
emptyList()
}
}
private fun pickPhotoViewModels(): List {
val pickAnImage = intentCreator.pickExistingImage()
return intentResolver.createViewModelsFor(pickAnImage, "")
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ui/AvatarPickerView.java
================================================
package com.alexstyl.specialdates.addevent.ui;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import com.alexstyl.specialdates.R;
import com.nostra13.universalimageloader.core.assist.ViewScaleType;
import com.nostra13.universalimageloader.core.imageaware.ImageAware;
import com.novoda.notils.caster.Views;
public final class AvatarPickerView extends RelativeLayout implements ImageAware {
private ImageView imageView;
private ImageView gradientTopView;
private ImageView gradientBottomView;
private View icon;
public AvatarPickerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
inflate(getContext(), R.layout.merge_avatar_picker_view, this);
icon = findViewById(R.id.avatar_picker_icon);
imageView = Views.findById(this, R.id.avatar_picker_image);
gradientTopView = Views.findById(this, R.id.avatar_picker_gradient__top);
gradientBottomView = Views.findById(this, R.id.avatar_picker_gradient__bottom);
}
@Override
public ViewScaleType getScaleType() {
return ViewScaleType.fromImageView(imageView);
}
@Override
public View getWrappedView() {
return imageView;
}
@Override
public boolean isCollected() {
return false;
}
@Override
public boolean setImageDrawable(Drawable drawable) {
if (drawable == null) {
hideGradient();
} else {
showGradient();
}
imageView.setImageDrawable(drawable);
return true;
}
private void hideGradient() {
gradientTopView.setVisibility(GONE);
gradientBottomView.setVisibility(GONE);
}
@Override
public boolean setImageBitmap(Bitmap imageBitmap) {
if (imageBitmap == null) {
setImageDrawable(null);
hideGradient();
} else {
imageView.setImageBitmap(imageBitmap);
showGradient();
}
return true;
}
private void showGradient() {
gradientTopView.setVisibility(VISIBLE);
gradientBottomView.setVisibility(VISIBLE);
}
public boolean isDisplayingAvatar() {
return imageView.getDrawable() != null;
}
@Override
public void setEnabled(boolean enabled) {
if (enabled) {
icon.setVisibility(VISIBLE);
} else {
icon.setVisibility(GONE);
}
super.setEnabled(enabled);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ui/ContactSuggestionView.java
================================================
package com.alexstyl.specialdates.addevent.ui;
import android.content.Context;
import android.support.transition.TransitionManager;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.LinearLayout;
import com.alexstyl.specialdates.AppComponent;
import com.alexstyl.specialdates.MementoApplication;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.addevent.ContactsSearch;
import com.alexstyl.specialdates.contact.Contact;
import com.alexstyl.specialdates.contact.ContactsProvider;
import com.alexstyl.specialdates.images.ImageLoader;
import com.alexstyl.specialdates.search.NameMatcher;
import com.novoda.notils.caster.Views;
import com.novoda.notils.logger.simple.Log;
import com.novoda.notils.meta.AndroidUtils;
import javax.inject.Inject;
public class ContactSuggestionView extends LinearLayout {
private OnContactSelectedListener listener = OnContactSelectedListener.NO_CALLBACKS;
private AutoCompleteTextView autoCompleteView;
@Inject ImageLoader imageLoader;
@Inject ContactsProvider contactsProvider;
public ContactSuggestionView(Context context, AttributeSet attrs) {
super(context, attrs);
if (!isInEditMode()) {
AppComponent applicationModule = ((MementoApplication) context.getApplicationContext()).getApplicationModule();
applicationModule.inject(this);
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
super.setOrientation(HORIZONTAL);
inflate(getContext(), R.layout.merge_contact_suggestion_view, this);
autoCompleteView = Views.findById(this, R.id.contact_suggestion_autocomplete);
if (isInEditMode()) {
return;
}
final View clearContact = findViewById(R.id.add_event_remove_contact);
clearContact.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
autoCompleteView.setText("");
clearContact.setVisibility(GONE);
listener.onContactCleared();
autoCompleteView.setEnabled(true);
autoCompleteView.getBackground().setAlpha(255);
autoCompleteView.requestFocus();
}
});
ContactsSearch contactsSearch = new ContactsSearch(contactsProvider, NameMatcher.INSTANCE);
final ContactsAdapter adapter = new ContactsAdapter(contactsSearch, imageLoader);
autoCompleteView.setAdapter(adapter);
autoCompleteView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
listener.onContactSelected(adapter.getItem(position));
AndroidUtils.requestHideKeyboard(view.getContext(), view);
TransitionManager.beginDelayedTransition(ContactSuggestionView.this);
clearContact.setVisibility(VISIBLE);
autoCompleteView.setEnabled(false);
autoCompleteView.getBackground().setAlpha(0);
}
});
}
public void addTextChangedListener(TextWatcher textWatcher) {
autoCompleteView.addTextChangedListener(textWatcher);
}
public void setOnContactSelectedListener(OnContactSelectedListener listener) {
this.listener = listener;
}
public interface OnContactSelectedListener {
void onContactSelected(Contact contact);
OnContactSelectedListener NO_CALLBACKS = new OnContactSelectedListener() {
@Override
public void onContactSelected(Contact contact) {
Log.w("onContactSelected called with no callbacks");
}
@Override
public void onContactCleared() {
Log.w("onContactCleared without a listener");
}
};
void onContactCleared();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ui/ContactsAdapter.java
================================================
package com.alexstyl.specialdates.addevent.ui;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.addevent.ContactsSearch;
import com.alexstyl.specialdates.contact.Contact;
import com.alexstyl.specialdates.images.ImageLoader;
import com.alexstyl.specialdates.ui.widget.ColorImageView;
import com.novoda.notils.caster.Views;
import java.util.ArrayList;
import java.util.List;
class ContactsAdapter extends BaseAdapter implements Filterable {
private final ImageLoader imageLoader;
private final ArrayList contacts = new ArrayList<>();
private final Filter filter;
ContactsAdapter(ContactsSearch contactsSearch, ImageLoader imageLoader) {
this.imageLoader = imageLoader;
this.filter = new DeviceContactsFilter(contactsSearch) {
@Override
public void onContactsFiltered(@NonNull List contacts) {
setSuggestions(contacts);
}
};
}
@Override
public int getCount() {
return contacts.size();
}
@Override
public Contact getItem(int position) {
return contacts.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
ContactViewHolder vh;
if (view == null) {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_contact, parent, false);
vh = new ContactViewHolder();
vh.contactName = Views.findById(view, R.id.display_name);
vh.avatar = Views.findById(view, R.id.search_result_avatar);
view.setTag(vh);
} else {
vh = (ContactViewHolder) view.getTag();
}
Contact contact = getItem(position);
String displayName = contact.toString();
vh.contactName.setText(displayName);
vh.avatar.setCircleColorVariant((int) contact.getContactID());
vh.avatar.setLetter(displayName);
imageLoader
.load(contact.getImagePath())
.asCircle()
.into(vh.avatar.getImageView());
return view;
}
private static class ContactViewHolder {
private ColorImageView avatar;
private TextView contactName;
}
private void setSuggestions(List contacts) {
this.contacts.clear();
this.contacts.addAll(contacts);
notifyDataSetChanged();
}
@Override
public Filter getFilter() {
return filter;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ui/DeviceContactsFilter.kt
================================================
package com.alexstyl.specialdates.addevent.ui
import android.widget.Filter
import com.alexstyl.specialdates.addevent.ContactsSearch
import com.alexstyl.specialdates.contact.Contact
import java.util.ArrayList
internal abstract class DeviceContactsFilter(private val contactsSearch: ContactsSearch) : Filter() {
override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
if (constraint == null || constraint.isEmpty()) {
return emptyResults()
}
val searchQuery = constraint.trim { it <= ' ' }.toString()
val contacts = contactsSearch.searchForContacts(searchQuery, LOAD_A_SINGLE_CONTACT)
val filterResults = Filter.FilterResults()
filterResults.values = contacts
filterResults.count = contacts.size
return filterResults
}
private fun emptyResults(): Filter.FilterResults {
val filterResults = Filter.FilterResults()
filterResults.values = ArrayList()
filterResults.count = 0
return filterResults
}
override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
onContactsFiltered(results.values as List)
}
abstract fun onContactsFiltered(contacts: List)
companion object {
private const val LOAD_A_SINGLE_CONTACT = 1
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/addevent/ui/EventDatePicker.java
================================================
package com.alexstyl.specialdates.addevent.ui;
import android.content.Context;
import android.support.transition.TransitionManager;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckedTextView;
import android.widget.LinearLayout;
import android.widget.NumberPicker;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.date.Date;
import com.alexstyl.specialdates.date.MonthInt;
import com.alexstyl.specialdates.upcoming.MonthLabels;
import com.novoda.notils.caster.Views;
import java.util.Locale;
public class EventDatePicker extends LinearLayout {
private static final int FIRST_DAY_OF_MONTH = 1;
private final MonthLabels labels;
private final NumberPicker dayPicker;
private final NumberPicker monthPicker;
private final NumberPicker yearPicker;
private final CheckedTextView includesYearCheckbox;
private final Date today;
private static final int FIRST_MONTH = 1;
private static final int LAST_MONTH = 12;
private static final int FIRST_YEAR = 1900;
public EventDatePicker(Context context, AttributeSet attrs) {
super(context, attrs);
super.setOrientation(HORIZONTAL);
labels = MonthLabels.forLocale(Locale.getDefault());
today = Date.Companion.today();
inflate(getContext(), R.layout.merge_birthday_picker, this);
dayPicker = Views.findById(this, R.id.day_picker);
setupDayPicker();
monthPicker = Views.findById(this, R.id.month_picker);
setupMonthPicker();
yearPicker = Views.findById(this, R.id.year_picker);
setupYearPicker();
includesYearCheckbox = Views.findById(this, R.id.include_year_checkbox);
includesYearCheckbox.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
boolean checked = includesYearCheckbox.isChecked();
includesYearCheckbox.setChecked(!checked);
if (includesYearCheckbox.isChecked()) {
showYearPicker();
} else {
hideYearPicker();
}
updateMaximumDaysInCurrentMonth();
}
private void hideYearPicker() {
TransitionManager.beginDelayedTransition(EventDatePicker.this);
yearPicker.setVisibility(GONE);
}
private void showYearPicker() {
TransitionManager.beginDelayedTransition(EventDatePicker.this);
yearPicker.setVisibility(VISIBLE);
}
});
}
private void setupDayPicker() {
dayPicker.setMinValue(FIRST_DAY_OF_MONTH);
dayPicker.setMaxValue(today.getDaysInCurrentMonth());
dayPicker.setValue(today.getDayOfMonth());
}
private void setupMonthPicker() {
monthPicker.setMinValue(FIRST_MONTH);
monthPicker.setMaxValue(LAST_MONTH);
monthPicker.setDisplayedValues(labels.getMonthsOfYear());
monthPicker.setValue(today.getMonth());
monthPicker.setOnValueChangedListener(dateValidator);
}
private void setupYearPicker() {
yearPicker.setMinValue(FIRST_YEAR);
yearPicker.setMaxValue(currentYear());
yearPicker.setValue(currentYear());
yearPicker.setOnValueChangedListener(dateValidator);
}
private Integer currentYear() {
return today.getYear();
}
public void setDisplayingDate(Date dateToDisplay) {
if (dateToDisplay.hasYear()) {
dayPicker.setValue(dateToDisplay.getDayOfMonth());
monthPicker.setValue(dateToDisplay.getMonth());
yearPicker.setValue(dateToDisplay.getYear());
yearPicker.setVisibility(VISIBLE);
includesYearCheckbox.setChecked(true);
} else {
dayPicker.setValue(dateToDisplay.getDayOfMonth());
monthPicker.setValue(dateToDisplay.getMonth());
yearPicker.setValue(currentYear());
yearPicker.setVisibility(GONE);
includesYearCheckbox.setChecked(false);
}
}
public Date getDisplayingDate() {
int dayOfMonth = getDayOfMonth();
int month = getMonth();
if (isDisplayingYear()) {
int year = getYear();
return Date.Companion.on(dayOfMonth, month, year);
} else {
return Date.Companion.on(dayOfMonth, month);
}
}
private boolean isDisplayingYear() {
return includesYearCheckbox.isChecked();
}
private int getDayOfMonth() {
return dayPicker.getValue();
}
@MonthInt
private int getMonth() {
@MonthInt int value = monthPicker.getValue();
return value;
}
private int getYear() {
return yearPicker.getValue();
}
private final NumberPicker.OnValueChangeListener dateValidator = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
updateMaximumDaysInCurrentMonth();
}
};
private void updateMaximumDaysInCurrentMonth() {
int maxDays;
if (isDisplayingYear()){
maxDays = Date.Companion.on(FIRST_DAY_OF_MONTH, getMonth(), getYear()).getDaysInCurrentMonth();
} else {
maxDays = Date.Companion.on(FIRST_DAY_OF_MONTH, getMonth()).getDaysInCurrentMonth();
}
dayPicker.setMaxValue(maxDays);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/analytics/Action.java
================================================
package com.alexstyl.specialdates.analytics;
public enum Action {
ADD_BIRTHDAY("add_bday"),
DAILY_REMINDER("reminder"),
DONATION("donate"),
INTERACT_CONTACT("contact"),
SELECT_THEME("theme"),
SELECT_DATE("select_date"),
COMPLICATION("complication: contacts events");
private final String name;
Action(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/analytics/ActionWithParameters.java
================================================
package com.alexstyl.specialdates.analytics;
public final class ActionWithParameters {
private final Action actionName;
private final String label;
private final String value;
public ActionWithParameters(Action actionName, String label, String value) {
this.actionName = actionName;
this.label = label;
this.value = value;
}
public String getName() {
return actionName.getName();
}
public String getLabel() {
return label;
}
public String getValue() {
return value;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/analytics/AnalyticsModule.kt
================================================
package com.alexstyl.specialdates.analytics
import android.content.Context
import com.alexstyl.specialdates.common.BuildConfig
import com.google.firebase.analytics.FirebaseAnalytics
import com.mixpanel.android.mpmetrics.MixpanelAPI
import javax.inject.Singleton
import dagger.Module
import dagger.Provides
@Module
@Singleton
class AnalyticsModule {
@Provides
@Singleton
internal fun providesAnalytics(context: Context): Analytics {
return CompositeAnalytics(buildMixPanel(context), buildFirebase(context))
}
private fun buildFirebase(context: Context) =
FirebaseAnalyticsImpl(FirebaseAnalytics.getInstance(context))
private fun buildMixPanel(context: Context): MixPanel {
val projectToken = BuildConfig.MIXPANEL_TOKEN
val mixpanel = MixpanelAPI.getInstance(context, projectToken)
val analytics = MixPanel(mixpanel)
return analytics
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/analytics/CompositeAnalytics.kt
================================================
package com.alexstyl.specialdates.analytics
import com.alexstyl.specialdates.TimeOfDay
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.donate.Donation
import com.alexstyl.specialdates.events.peopleevents.EventType
class CompositeAnalytics(private vararg val analytics: Analytics) : Analytics {
override fun trackThemeSelected(string: String) {
analytics.forEach {
it.trackThemeSelected(string)
}
}
override fun trackScreen(screen: Screen) {
analytics.forEach {
it.trackScreen(screen)
}
}
override fun trackAddEventsCancelled() {
analytics.forEach {
it.trackAddEventsCancelled()
}
}
override fun trackEventAddedSuccessfully() {
analytics.forEach {
it.trackEventAddedSuccessfully()
}
}
override fun trackContactSelected() {
analytics.forEach {
it.trackContactSelected()
}
}
override fun trackEventDatePicked(eventType: EventType) {
analytics.forEach {
it.trackEventDatePicked(eventType)
}
}
override fun trackEventRemoved(eventType: EventType) {
analytics.forEach {
it.trackEventRemoved(eventType)
}
}
override fun trackImageCaptured() {
analytics.forEach {
it.trackImageCaptured()
}
}
override fun trackExistingImagePicked() {
analytics.forEach {
it.trackExistingImagePicked()
}
}
override fun trackAvatarSelected() {
analytics.forEach {
it.trackAvatarSelected()
}
}
override fun trackContactUpdated() {
analytics.forEach {
it.trackContactUpdated()
}
}
override fun trackContactCreated() {
analytics.forEach {
it.trackContactCreated()
}
}
override fun trackDailyReminderEnabled() {
analytics.forEach {
it.trackDailyReminderEnabled()
}
}
override fun trackDailyReminderDisabled() {
analytics.forEach {
it.trackDailyReminderDisabled()
}
}
override fun trackDailyReminderTimeUpdated(timeOfDay: TimeOfDay) {
analytics.forEach {
it.trackDailyReminderTimeUpdated(timeOfDay)
}
}
override fun trackWidgetAdded(widget: Widget) {
analytics.forEach {
it.trackWidgetAdded(widget)
}
}
override fun trackWidgetRemoved(widget: Widget) {
analytics.forEach {
it.trackWidgetRemoved(widget)
}
}
override fun trackDonationStarted(donation: Donation) {
analytics.forEach {
it.trackDonationStarted(donation)
}
}
override fun trackAppInviteRequested() {
analytics.forEach {
it.trackAppInviteRequested()
}
}
override fun trackDonationRestored() {
analytics.forEach {
it.trackDonationRestored()
}
}
override fun trackDonationPlaced(donation: Donation) {
analytics.forEach {
it.trackDonationPlaced(donation)
}
}
override fun trackFacebookLoggedIn() {
analytics.forEach {
it.trackFacebookLoggedIn()
}
}
override fun trackOnAvatarBounce() {
analytics.forEach {
it.trackOnAvatarBounce()
}
}
override fun trackFacebookLoggedOut() {
analytics.forEach {
it.trackFacebookLoggedOut()
}
}
override fun trackVisitGithub() {
analytics.forEach {
it.trackVisitGithub()
}
}
override fun trackContactDetailsViewed(contact: Contact) {
analytics.forEach {
it.trackContactDetailsViewed(contact)
}
}
override fun trackNamedaysScreen() {
analytics.forEach {
it.trackNamedaysScreen()
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/analytics/FirebaseAnalyticsImpl.kt
================================================
package com.alexstyl.specialdates.analytics
import android.os.Bundle
import android.support.annotation.NonNull
import android.support.annotation.Size
import com.alexstyl.specialdates.TimeOfDay
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.donate.Donation
import com.alexstyl.specialdates.events.peopleevents.EventType
import com.google.firebase.analytics.FirebaseAnalytics
class FirebaseAnalyticsImpl(private val firebase: FirebaseAnalytics) : Analytics {
override fun trackThemeSelected(string: String) {
firebase.logEvent(Action.SELECT_THEME.name, Bundle().apply {
putString("theme name", string)
})
}
override fun trackScreen(screen: Screen) {
val bundle = Bundle()
bundle.putString("screen_name", screen.screenName())
firebase.logEvent("screen_view", bundle)
}
override fun trackAddEventsCancelled() {
firebase.logEvent("add_events_cancelled")
}
override fun trackEventAddedSuccessfully() {
firebase.logEvent("add_events_success")
}
override fun trackContactSelected() {
firebase.logEvent("contact_selected")
}
override fun trackEventDatePicked(eventType: EventType) {
val properties = createPropertyFor(eventType)
firebase.logEvent("event_date_picked", properties)
}
override fun trackEventRemoved(eventType: EventType) {
val properties = createPropertyFor(eventType)
firebase.logEvent("event_removed", properties)
}
override fun trackImageCaptured() {
firebase.logEvent("image_captured")
}
override fun trackExistingImagePicked() {
firebase.logEvent("existing_image_picked")
}
override fun trackAvatarSelected() {
firebase.logEvent("avatar_selected")
}
override fun trackContactUpdated() {
firebase.logEvent("contact_updated")
}
override fun trackContactCreated() {
firebase.logEvent("contact_created")
}
override fun trackDailyReminderEnabled() {
firebase.logEvent("daily_reminder_enabled")
}
override fun trackDailyReminderDisabled() {
firebase.logEvent("daily_reminder_disabled")
}
override fun trackDailyReminderTimeUpdated(timeOfDay: TimeOfDay) {
val properties = createPropertyFor(timeOfDay)
firebase.logEvent("daily_reminder_time_updated", properties)
}
override fun trackWidgetAdded(widget: Widget) {
firebase.logEvent("widget_added", widgetNameOf(widget))
}
private fun widgetNameOf(widget: Widget): Bundle {
return Bundle().apply {
putString("widget_name", widget.widgetName)
}
}
override fun trackWidgetRemoved(widget: Widget) {
firebase.logEvent("widget_removed", widgetNameOf(widget))
}
override fun trackDonationStarted(donation: Donation) {
val properties = Bundle().apply {
putString(FirebaseAnalytics.Param.ITEM_ID, donation.identifier)
putString(FirebaseAnalytics.Param.PRICE, donation.amount)
}
firebase.logEvent("donation_started", properties)
}
override fun trackAppInviteRequested() {
firebase.logEvent("app_invite_requested")
}
override fun trackDonationRestored() {
firebase.logEvent("donation_restored")
}
override fun trackDonationPlaced(donation: Donation) {
val properties = Bundle().apply {
putString(FirebaseAnalytics.Param.ITEM_ID, donation.identifier)
putString(FirebaseAnalytics.Param.PRICE, donation.amount)
}
firebase.logEvent(FirebaseAnalytics.Event.ECOMMERCE_PURCHASE, properties)
}
override fun trackFacebookLoggedIn() {
firebase.logEvent("facebook_log_in")
}
override fun trackOnAvatarBounce() {
firebase.logEvent("avatar_bounce")
}
override fun trackFacebookLoggedOut() {
firebase.logEvent("facebook_log_out")
}
override fun trackVisitGithub() {
firebase.logEvent("visit_github")
}
override fun trackContactDetailsViewed(contact: Contact) {
firebase.logEvent("view_contact_details")
}
override fun trackNamedaysScreen() {
firebase.logEvent("namedays_screen")
}
private fun createPropertyFor(eventType: EventType): Bundle {
val properties = Bundle()
properties.putInt("event_type", eventType.id)
return properties
}
private fun createPropertyFor(timeOfDay: TimeOfDay): Bundle {
return Bundle().apply {
putString("time", timeOfDay.toString())
}
}
private fun FirebaseAnalytics.logEvent(@NonNull @Size(min = 1L, max = 40L) eventName: String) {
return logEvent(eventName, Bundle.EMPTY)
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/analytics/MixPanel.kt
================================================
package com.alexstyl.specialdates.analytics
import com.alexstyl.specialdates.TimeOfDay
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.donate.Donation
import com.alexstyl.specialdates.events.peopleevents.EventType
import com.mixpanel.android.mpmetrics.MixpanelAPI
import org.json.JSONException
import org.json.JSONObject
class MixPanel(private val mixpanel: MixpanelAPI) : Analytics {
override fun trackThemeSelected(string: String) {
mixpanel.track(Action.SELECT_THEME.name, JSONObject().apply {
put("theme name", string)
})
}
override fun trackScreen(screen: Screen) {
mixpanel.track("ScreenView: " + screen.screenName())
}
override fun trackAddEventsCancelled() {
mixpanel.track("add events cancelled")
}
override fun trackEventAddedSuccessfully() {
mixpanel.track("add events success")
}
override fun trackContactSelected() {
mixpanel.track("contact selected")
}
override fun trackEventDatePicked(eventType: EventType) {
val properties = createPropertyFor(eventType)
mixpanel.track("event date picked ", properties)
}
override fun trackEventRemoved(eventType: EventType) {
val properties = createPropertyFor(eventType)
mixpanel.track("event removed", properties)
}
override fun trackImageCaptured() {
mixpanel.track("image captured")
}
override fun trackExistingImagePicked() {
mixpanel.track("existing image picked")
}
override fun trackAvatarSelected() {
mixpanel.track("avatar selected")
}
override fun trackContactUpdated() {
mixpanel.track("contact updated")
}
override fun trackContactCreated() {
mixpanel.track("contact created")
}
override fun trackDailyReminderEnabled() {
mixpanel.track("daily reminder enabled")
}
override fun trackDailyReminderDisabled() {
mixpanel.track("daily reminder disabled")
}
override fun trackDailyReminderTimeUpdated(timeOfDay: TimeOfDay) {
val properties = createPropertyFor(timeOfDay)
mixpanel.track("daily reminder time updated", properties)
}
override fun trackWidgetAdded(widget: Widget) {
mixpanel.track("widget_added", widgetNameOf(widget))
}
private fun widgetNameOf(widget: Widget): JSONObject {
val properties = JSONObject()
try {
properties.put("widget_name", widget.widgetName)
return properties
} catch (e: JSONException) {
e.printStackTrace()
}
return properties
}
override fun trackWidgetRemoved(widget: Widget) {
mixpanel.track("widget_removed", widgetNameOf(widget))
}
override fun trackDonationStarted(donation: Donation) {
val properties = JSONObject()
try {
properties.put("id", donation.identifier)
properties.put("amount", donation.amount)
} catch (e: JSONException) {
e.printStackTrace()
}
mixpanel.track("donation started", properties)
}
override fun trackAppInviteRequested() {
mixpanel.track("app_invite_requested")
}
override fun trackDonationRestored() {
mixpanel.track("donation_restored")
}
override fun trackDonationPlaced(donation: Donation) {
val properties = JSONObject()
try {
properties.put("amount", donation.amount)
properties.put("identifier", donation.identifier)
} catch (e: JSONException) {
e.printStackTrace()
}
mixpanel.track("donation_placed", properties)
}
override fun trackFacebookLoggedIn() {
mixpanel.track("facebook_log_in")
}
override fun trackOnAvatarBounce() {
mixpanel.track("avatar bounce")
}
override fun trackFacebookLoggedOut() {
mixpanel.track("facebook_log_out")
}
override fun trackVisitGithub() {
mixpanel.track("visit_github")
}
override fun trackContactDetailsViewed(contact: Contact) {
mixpanel.track("view_contact_details")
}
override fun trackNamedaysScreen() {
mixpanel.track("namedays_screen")
}
private fun createPropertyFor(eventType: EventType): JSONObject {
val properties = JSONObject()
try {
properties.put("event type", eventType.id)
} catch (e: JSONException) {
e.printStackTrace()
}
return properties
}
private fun createPropertyFor(timeOfDay: TimeOfDay): JSONObject {
val properties = JSONObject()
try {
properties.put("time", timeOfDay.toString())
} catch (e: JSONException) {
e.printStackTrace()
}
return properties
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/contact/AndroidContactFactory.kt
================================================
package com.alexstyl.specialdates.contact
import android.content.ContentResolver
import android.content.ContentUris
import android.database.Cursor
import android.provider.ContactsContract
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.contact.AndroidContactsQuery.SORT_ORDER
import com.alexstyl.specialdates.contact.ContactSource.SOURCE_DEVICE
import java.net.URI
class AndroidContactFactory(private val resolver: ContentResolver, private val tracker: CrashAndErrorTracker) {
fun getAllContacts(): Contacts {
val cursor: Cursor?
try {
cursor = resolver.query(
AndroidContactsQuery.CONTENT_URI,
AndroidContactsQuery.PROJECTION,
WHERE, null,
AndroidContactsQuery.SORT_ORDER
)
} catch (e: Exception) {
tracker.track(e)
return Contacts(SOURCE_DEVICE, emptyList())
}
return cursor.use {
return@use Contacts(SOURCE_DEVICE, List(it.count, { index ->
it.moveToPosition(index)
createContactFrom(it)
}))
}
}
@Throws(ContactNotFoundException::class)
fun createContactWithId(contactID: Long): Contact {
val cursor = queryContactsWithContactId(contactID)
if (isInvalid(cursor)) {
throw RuntimeException("Cursor was invalid")
}
cursor.use {
if (it.moveToFirst()) {
return createContactFrom(it)
}
}
throw ContactNotFoundException(contactID)
}
fun queryContacts(ids: List): Contacts {
val cursor = queryContactsWithContactId(ids)
return cursor.use {
return@use Contacts(SOURCE_DEVICE, List(it.count) { index ->
it.moveToPosition(index)
createContactFrom(it)
})
}
}
private fun queryContactsWithContactId(ids: List): Cursor {
return resolver.query(
AndroidContactsQuery.CONTENT_URI,
AndroidContactsQuery.PROJECTION,
"${AndroidContactsQuery._ID} IN (${ids.joinToString(",")})",
null,
SORT_ORDER
)
}
private fun createContactFrom(cursor: Cursor): Contact {
val contactID = getContactIdFrom(cursor)
val displayName = getDisplayNameFrom(cursor)
val imagePath = URI.create(ContentUris.withAppendedId(AndroidContactsQuery.CONTENT_URI, contactID).toString())
return Contact(contactID, displayName, imagePath, SOURCE_DEVICE)
}
private fun queryContactsWithContactId(contactID: Long): Cursor {
return resolver.query(
AndroidContactsQuery.CONTENT_URI,
AndroidContactsQuery.PROJECTION,
SELECTION_CONTACT_WITH_ID,
makeSelectionArgumentsFor(contactID),
SORT_ORDER + " LIMIT 1"
)
}
private fun makeSelectionArgumentsFor(contactID: Long): Array =
arrayOf(contactID.toString())
companion object {
private val WHERE = ContactsContract.Data.IN_VISIBLE_GROUP + "=1"
private val SELECTION_CONTACT_WITH_ID = AndroidContactsQuery._ID + " = ?"
private fun isInvalid(cursor: Cursor?): Boolean = cursor == null || cursor.isClosed
private fun getContactIdFrom(cursor: Cursor): Long =
cursor.getLong(AndroidContactsQuery.CONTACT_ID)
private fun getDisplayNameFrom(cursor: Cursor): DisplayName =
DisplayName.from(cursor.getString(AndroidContactsQuery.DISPLAY_NAME))
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/contact/AndroidContactsProviderSource.kt
================================================
package com.alexstyl.specialdates.contact
internal class AndroidContactsProviderSource(private val cache: ContactCache, private val factory: AndroidContactFactory) : ContactsProviderSource {
@Throws(ContactNotFoundException::class)
override fun getOrCreateContact(contactID: Long): Contact {
var deviceContact = cache.getContact(contactID)
if (deviceContact == null) {
deviceContact = factory.createContactWithId(contactID)
cache.addContact(deviceContact)
}
return deviceContact
}
override val allContacts: Contacts
get() {
val allContacts = factory.getAllContacts()
cache.evictAll()
cache.addContacts(allContacts)
return allContacts
}
override fun queryContacts(contactIds: List): Contacts {
val contacts = factory.queryContacts(contactIds)
cache.addContacts(contacts)
return contacts
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/contact/AndroidContactsQuery.java
================================================
package com.alexstyl.specialdates.contact;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
final class AndroidContactsQuery {
public static final Uri CONTENT_URI = ContactsContract.Contacts.CONTENT_URI;
static final String[] PROJECTION = {
Contacts._ID,
Contacts.DISPLAY_NAME_PRIMARY,
};
static final String SORT_ORDER = Contacts._ID;
public static final int CONTACT_ID = 0;
static final int DISPLAY_NAME = 1;
public static final String _ID = Contacts._ID;
private AndroidContactsQuery() {
// hide this
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/contact/ContactIntentExtractor.kt
================================================
package com.alexstyl.specialdates.contact
import android.content.Intent
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.MementoConstants
import com.alexstyl.specialdates.Optional
class ContactIntentExtractor(val tracker: CrashAndErrorTracker,
val contactsProvider: ContactsProvider) {
companion object {
val EXTRA_CONTACT_SOURCE = "${MementoConstants.PACKAGE}.extra:source"
val EXTRA_CONTACT_ID = "${MementoConstants.PACKAGE}.extra:contactId"
}
fun getContactExtra(intent: Intent): Optional {
val contactID = intent.getLongExtra(EXTRA_CONTACT_ID, -1)
if (contactID == -1L) {
return Optional.absent()
}
@ContactSource val contactSource = intent.getIntExtra(EXTRA_CONTACT_SOURCE, -1)
return if (contactSource == -1) {
return Optional.absent()
} else contactFor(contactID, contactSource)
}
private fun contactFor(contactID: Long, contactSource: Int): Optional {
return try {
Optional(contactsProvider.getContact(contactID, contactSource))
} catch (e: ContactNotFoundException) {
tracker.track(e)
Optional.absent()
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/contact/ContactsModule.kt
================================================
package com.alexstyl.specialdates.contact
import android.content.ContentResolver
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.contact.ContactSource.SOURCE_DEVICE
import com.alexstyl.specialdates.contact.ContactSource.SOURCE_FACEBOOK
import com.alexstyl.specialdates.events.database.EventSQLiteOpenHelper
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class ContactsModule {
@Provides
@Singleton
internal fun provider(contentResolver: ContentResolver,
eventSQLiteOpenHelper: EventSQLiteOpenHelper,
tracker: CrashAndErrorTracker): ContactsProvider {
return ContactsProvider(mapOf(
Pair(SOURCE_DEVICE, buildAndroidSource(tracker, contentResolver)),
Pair(SOURCE_FACEBOOK, buildFacebookSource(eventSQLiteOpenHelper))
))
}
companion object {
private const val CACHE_SIZE = 1024
private fun buildAndroidSource(tracker: CrashAndErrorTracker, contentResolver: ContentResolver): ContactsProviderSource {
val factory = AndroidContactFactory(contentResolver, tracker)
val contactCache = ContactCache(CACHE_SIZE)
return AndroidContactsProviderSource(contactCache, factory)
}
private fun buildFacebookSource(eventSQLHelper: EventSQLiteOpenHelper): ContactsProviderSource {
val contactCache = ContactCache(CACHE_SIZE)
return FacebookContactsSource(eventSQLHelper, contactCache)
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/contact/EmptyContactSource.kt
================================================
package com.alexstyl.specialdates.contact
class EmptyContactSource(val source: Int) : ContactsProviderSource {
override val allContacts: Contacts
get() = Contacts(source, emptyList())
@Throws(ContactNotFoundException::class)
override fun getOrCreateContact(contactID: Long): Contact {
throw ContactNotFoundException(contactID)
}
override fun queryContacts(contactIds: List): Contacts = Contacts(source, emptyList())
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/contact/FacebookContactsSource.kt
================================================
package com.alexstyl.specialdates.contact
import android.database.Cursor
import com.alexstyl.specialdates.contact.ContactSource.SOURCE_FACEBOOK
import com.alexstyl.specialdates.events.database.DatabaseContract.AnnualEventsContract
import com.alexstyl.specialdates.events.database.EventSQLiteOpenHelper
import com.alexstyl.specialdates.facebook.FacebookImagePath
internal class FacebookContactsSource(private val eventSQLHelper: EventSQLiteOpenHelper,
private val cache: ContactCache)
: ContactsProviderSource {
@Throws(ContactNotFoundException::class)
override fun getOrCreateContact(contactID: Long): Contact {
var contact: Contact? = cache.getContact(contactID)
if (contact == null) {
contact = queryContactWith(contactID)
}
return contact
}
@Throws(ContactNotFoundException::class)
private fun queryContactWith(contactID: Long): Contact {
val readableDatabase = eventSQLHelper.readableDatabase
val cursor = readableDatabase.query(
AnnualEventsContract.TABLE_NAME, null,
"$IS_A_FACEBOOK_CONTACT AND ${AnnualEventsContract.CONTACT_ID} == $contactID", null, null, null, null
)
return cursor.use {
if (!it.moveToFirst()) {
throw ContactNotFoundException(contactID)
}
return@use createContactFrom(cursor)
}
}
override fun queryContacts(contactIds: List): Contacts {
val readableDatabase = eventSQLHelper.readableDatabase
val cursor = readableDatabase.query(
AnnualEventsContract.TABLE_NAME,
null,
"$IS_A_FACEBOOK_CONTACT AND ${AnnualEventsContract.CONTACT_ID} IN (${List(contactIds.size, { "?" }).joinToString(",")})",
contactIds.map { it.toString() }.toTypedArray(),
null,
null,
null
)
return cursor.use {
val contacts = Contacts(SOURCE_FACEBOOK, List(it.count, { index ->
it.moveToPosition(index)
createContactFrom(it)
}))
cache.addContacts(contacts)
return@use contacts
}
}
override val allContacts: Contacts
get() {
return queryAllContacts().apply {
cache.evictAll()
cache.addContacts(this)
}
}
private fun queryAllContacts(): Contacts {
val db = eventSQLHelper.readableDatabase
val cursor = db.rawQuery(
"SELECT * FROM ${AnnualEventsContract.TABLE_NAME}" +
" WHERE ${AnnualEventsContract.SOURCE} == ? " +
" GROUP BY ${AnnualEventsContract.CONTACT_ID}",
arrayOf(SOURCE_FACEBOOK.toString()))
return cursor.use {
return@use Contacts(SOURCE_FACEBOOK, List(it.count, { index ->
it.moveToPosition(index)
createContactFrom(it)
}))
}
}
companion object {
private const val IS_A_FACEBOOK_CONTACT = AnnualEventsContract.SOURCE + "== " + SOURCE_FACEBOOK
private fun createContactFrom(cursor: Cursor): Contact {
val uid = cursor.getLong(cursor.getColumnIndexOrThrow(AnnualEventsContract.CONTACT_ID))
val displayName = DisplayName.from(cursor.getString(cursor.getColumnIndexOrThrow(AnnualEventsContract.DISPLAY_NAME)))
val imagePath = FacebookImagePath.forUid(uid)
return Contact(uid, displayName, imagePath, SOURCE_FACEBOOK)
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/AlarmManagerCompat.java
================================================
package com.alexstyl.specialdates.dailyreminder;
import android.app.AlarmManager;
import android.app.PendingIntent;
import com.alexstyl.android.Version;
public final class AlarmManagerCompat {
private final AlarmManager alarmManager;
public AlarmManagerCompat(AlarmManager alarmManager) {
this.alarmManager = alarmManager;
}
public void setExact(int type, long triggerAtmillis, PendingIntent operation) {
if (Version.INSTANCE.hasMarshmallow()) {
alarmManager.setAndAllowWhileIdle(type, triggerAtmillis, operation);
} else if (Version.INSTANCE.hasKitKat()) {
alarmManager.setExact(type, triggerAtmillis, operation);
} else {
alarmManager.set(AlarmManager.RTC, triggerAtmillis, operation);
}
}
public void cancel(PendingIntent pendingIntent) {
alarmManager.cancel(pendingIntent);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/AndroidDailyReminderNotifier.kt
================================================
package com.alexstyl.specialdates.dailyreminder
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.RectF
import android.provider.ContactsContract.Contacts.CONTENT_LOOKUP_URI
import android.support.v4.app.NotificationCompat
import com.alexstyl.android.Version
import com.alexstyl.resources.Colors
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.dailyreminder.actions.PersonActionsActivity
import com.alexstyl.specialdates.events.namedays.activity.NamedaysOnADayActivity
import com.alexstyl.specialdates.home.HomeActivity
import com.alexstyl.specialdates.images.ImageLoader
import com.alexstyl.specialdates.person.PersonActivity
import java.net.URI
class AndroidDailyReminderNotifier(private val context: Context,
private val notificationManager: NotificationManager,
private val imageLoader: ImageLoader,
private val colors: Colors) : DailyReminderNotifier {
override fun notifyFor(viewModel: DailyReminderViewModel) {
if (viewModel.contacts.isNotEmpty()) {
when {
supportsNotificationGroupping() -> {
notifyContacts(viewModel.contacts)
notifySummary(viewModel)
}
viewModel.contacts.size == 1 -> notifyContacts(viewModel.contacts)
else -> notifySummary(viewModel)
}
}
if (viewModel.namedays.isPresent) {
notifyNamedays(viewModel.namedays.get())
}
if (viewModel.bankHoliday.isPresent) {
notifyBankHolidays(viewModel.bankHoliday.get())
}
}
private fun supportsNotificationGroupping() = Version.hasOreo()
private fun notifyContacts(viewModels: List) {
viewModels.forEach { viewModel ->
val requestCode = NotificationConstants.CHANNEL_ID_CONTACTS.hashCode() + viewModel.hashCode()
val startIntent = PersonActivity.buildIntentFor(context, viewModel.contact)
val pendingIntent = PendingIntent.getActivity(
context,
requestCode,
startIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
val notification =
NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID_CONTACTS)
.setContentTitle(viewModel.title)
.setContentText(viewModel.label)
.setContentIntent(pendingIntent)
.setActions(viewModel)
.loadLargeImage(viewModel.contact.imagePath)
.setSmallIcon(R.drawable.ic_stat_memento)
.setColor(colors.getDailyReminderColor())
.setGroup(NotificationConstants.DAILY_REMINDER_GROUP_ID)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setAutoCancel(true)
.addPerson(CONTENT_LOOKUP_URI.buildUpon().appendPath(viewModel.contact.contactID.toString()).build().toString())
.build()
notificationManager.notify(viewModel.notificationId, notification)
}
}
private fun notifySummary(viewModel: DailyReminderViewModel) {
val startIntent = HomeActivity.getStartIntent(context)
val requestCode = NotificationConstants.NOTIFICATION_ID_CONTACTS_SUMMARY
val pendingIntent = PendingIntent.getActivity(
context,
requestCode,
startIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
val summary = viewModel.summaryViewModel
val notification =
NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID_CONTACTS)
.setContentTitle(summary.title)
.setContentText(summary.text)
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_stat_memento)
.setColor(colors.getDailyReminderColor())
.setGroupSummary(true)
.setGroup(NotificationConstants.DAILY_REMINDER_GROUP_ID)
.setInboxStyle(viewModel.summaryViewModel)
.build()
notificationManager.notify(summary.notificationId, notification)
}
private fun notifyBankHolidays(bankHoliday: BankHolidayNotificationViewModel) {
val startIntent = HomeActivity.getStartIntent(context)
val requestCode = NotificationConstants.NOTIFICATION_ID_BANK_HOLIDAY
val pendingIntent = PendingIntent.getActivity(
context,
requestCode,
startIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
val notification =
NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID_BANKHOLIDAY)
.setContentTitle(bankHoliday.title)
.setContentText(bankHoliday.label)
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_stat_bankholidays)
.setColor(colors.getBankholidaysColor())
.build()
notificationManager.notify(NotificationConstants.NOTIFICATION_ID_BANK_HOLIDAY, notification)
}
private fun notifyNamedays(namedays: NamedaysNotificationViewModel) {
val startIntent = NamedaysOnADayActivity.getStartIntent(context, namedays.date)
val requestCode = NotificationConstants.NOTIFICATION_ID_NAMEDAYS
val pendingIntent = PendingIntent.getActivity(
context,
requestCode,
startIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
val notification =
NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID_NAMEDAYS)
.setContentTitle(namedays.title)
.setContentText(namedays.label)
.setStyle(NotificationCompat.BigTextStyle().bigText(namedays.label))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_stat_namedays)
.setColor(colors.getNamedaysColor())
.build()
notificationManager.notify(NotificationConstants.NOTIFICATION_ID_NAMEDAYS, notification)
}
override fun cancelAllEvents() {
notificationManager.cancel(NotificationConstants.NOTIFICATION_ID_CONTACTS_SUMMARY)
notificationManager.cancel(NotificationConstants.NOTIFICATION_ID_BANK_HOLIDAY)
notificationManager.cancel(NotificationConstants.NOTIFICATION_ID_NAMEDAYS)
}
private fun NotificationCompat.Builder.loadLargeImage(imagePath: URI): NotificationCompat.Builder = apply {
val width = context.resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width)
val height = context.resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height)
val bitmap =
imageLoader
.load(imagePath)
.withSize(width, height)
.synchronously()
if (bitmap.isPresent) {
if (Version.hasLollipop()) {
setLargeIcon(bitmap.get().toCircle())
} else {
setLargeIcon(bitmap.get())
}
}
}
private fun Bitmap.toCircle(): Bitmap? {
val output = Bitmap.createBitmap(
this.width,
this.height,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(output)
val color = Color.RED
val paint = Paint()
val rect = Rect(0, 0, this.width, this.height)
val rectF = RectF(rect)
paint.isAntiAlias = true
canvas.drawARGB(0, 0, 0, 0)
paint.color = color
canvas.drawOval(rectF, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(this, rect, rect, paint)
return output
}
private fun NotificationCompat.Builder.setActions(contactEventViewModel: ContactEventNotificationViewModel): NotificationCompat.Builder {
contactEventViewModel.actions.forEach { actionViewModel ->
val intent = buildIntentFor(actionViewModel, contactEventViewModel)
val pendingIntent = PendingIntent.getActivity(context, actionViewModel.id, intent, 0)
addAction(NotificationCompat.Action(0, actionViewModel.label, pendingIntent))
}
return this
}
private fun buildIntentFor(actionViewModel: ContactActionViewModel, contactEventViewModel: ContactEventNotificationViewModel): Intent =
when (actionViewModel.type) {
ActionType.CALL -> PersonActionsActivity.buildCallIntentFor(context, contactEventViewModel.contact)
ActionType.SEND_WISH -> PersonActionsActivity.buildSendIntentFor(context, contactEventViewModel.contact)
}
}
private fun NotificationCompat.Builder.setInboxStyle(viewModel: SummaryNotificationViewModel): NotificationCompat.Builder {
val inboxStyle = NotificationCompat.InboxStyle()
viewModel.lines.forEach { line ->
inboxStyle.addLine(line)
}
setStyle(inboxStyle)
return this
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/AndroidDailyReminderScheduler.kt
================================================
package com.alexstyl.specialdates.dailyreminder
import com.alexstyl.specialdates.TimeOfDay
import com.evernote.android.job.DailyJob
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest
import java.util.concurrent.TimeUnit
class AndroidDailyReminderScheduler : DailyReminderScheduler {
@JvmField
val ONE_HOUR = TimeUnit.HOURS.toMillis(1)
override fun scheduleReminderFor(timeOfDay: TimeOfDay) {
DailyJob.schedule(JobRequest.Builder(DailyReminderJob.TAG),
timeOfDay.toMillis(),
timeOfDay.toMillis() + ONE_HOUR
)
}
override fun cancelReminder() {
JobManager.instance().cancelAllForTag(DailyReminderJob.TAG)
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/AndroidDailyReminderViewModelFactory.kt
================================================
package com.alexstyl.specialdates.dailyreminder
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import com.alexstyl.resources.Colors
import com.alexstyl.specialdates.Strings
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.date.ContactEvent
import com.alexstyl.specialdates.date.Date
import com.alexstyl.specialdates.events.bankholidays.BankHoliday
import com.alexstyl.specialdates.events.namedays.NamesInADate
import com.alexstyl.specialdates.events.peopleevents.StandardEventType
import com.alexstyl.specialdates.util.NaturalLanguageUtils
import java.net.URI
class AndroidDailyReminderViewModelFactory(private val strings: Strings,
private val todaysDate: Date,
private val colors: Colors)
: DailyReminderViewModelFactory {
override fun summaryOf(viewModels: List): SummaryNotificationViewModel {
val contacts = viewModels.fold(emptyList(), { list, viewModel ->
list + viewModel.contact
})
val title = NaturalLanguageUtils.joinContacts(strings, contacts, MAX_CONTACTS)
val label = strings.dontForgetToSendWishes()
val lines = arrayListOf()
viewModels.forEach { contactViewModel ->
val boldedTitle = SpannableString("${contactViewModel.title}\t\t${contactViewModel.label}").apply {
setSpan(StyleSpan(Typeface.BOLD), 0, contactViewModel.title.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
lines.add(boldedTitle)
}
val images = viewModels.fold(emptyList(), { list, viewModel ->
list + viewModel.contact.imagePath
})
return SummaryNotificationViewModel(
NotificationConstants.NOTIFICATION_ID_CONTACTS_SUMMARY,
title, label, lines, images
)
}
override fun viewModelFor(contact: Contact, events: List): ContactEventNotificationViewModel {
val stringBuilder = SpannableStringBuilder()
events.forEach { contactEvent ->
val coloredLabel = SpannableString(contactEvent.getLabel(todaysDate, strings) + " " + emojiFor(contactEvent)).apply {
setSpan(ForegroundColorSpan(colors.getColorFor(contactEvent.type)), 0, this.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
if (stringBuilder.isNotEmpty()) {
stringBuilder.append(", ")
}
stringBuilder.append(coloredLabel)
}
return ContactEventNotificationViewModel(events.hashCode(),
contact,
contact.displayName.toString(),
stringBuilder,
emptyList() // TODO feature coming from the notification_actions branch
)
}
private fun emojiFor(contactEvent: ContactEvent): CharSequence =
when (contactEvent.type) {
StandardEventType.BIRTHDAY -> "🍰"
StandardEventType.NAMEDAY -> "🎈"
StandardEventType.ANNIVERSARY -> "💍"
StandardEventType.OTHER -> "🌸"
else -> ""
}
override fun viewModelFor(namedays: NamesInADate): NamedaysNotificationViewModel {
return NamedaysNotificationViewModel(namedays.date,
strings.todaysNamedays(namedays.getNames().size),
namedays.getNames().joinToString(", "))
}
override fun viewModelFor(bankHoliday: BankHoliday): BankHolidayNotificationViewModel =
BankHolidayNotificationViewModel(bankHoliday.holidayName, strings.bankholidaySubtitle())
companion object {
private const val MAX_CONTACTS = 3
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/DailyReminderDebugPreferences.java
================================================
package com.alexstyl.specialdates.dailyreminder;
import android.content.Context;
import android.support.v4.util.Pair;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.date.Date;
import com.alexstyl.specialdates.date.MonthInt;
import com.alexstyl.specialdates.date.Months;
public final class DailyReminderDebugPreferences {
private final EasyPreferences preferences;
public static DailyReminderDebugPreferences newInstance(Context context) {
return new DailyReminderDebugPreferences(EasyPreferences.createForPrivatePreferences(context, R.string.pref_dailyreminder_debug));
}
private DailyReminderDebugPreferences(EasyPreferences preferences) {
this.preferences = preferences;
}
@SuppressWarnings("magicNumber")
public Date getSelectedDate() {
int dayOfMonth = preferences.getInt(R.string.key_debug_daily_reminder_date_fake_day, 1);
@MonthInt int month = preferences.getInt(R.string.key_debug_daily_reminder_date_fake_month, Months.JANUARY);
int year = preferences.getInt(R.string.key_debug_daily_reminder_date_fake_year, 2016);
return Date.Companion.on(dayOfMonth, month, year);
}
boolean isFakeDateEnabled() {
return preferences.getBoolean(R.string.key_debug_daily_reminder_date_enable, false);
}
public void setSelectedDate(int dayOfMonth, int month, int year) {
Pair dayPair = new Pair<>(R.string.key_debug_daily_reminder_date_fake_day, dayOfMonth);
Pair monthPair = new Pair<>(R.string.key_debug_daily_reminder_date_fake_month, month);
Pair yearPair = new Pair<>(R.string.key_debug_daily_reminder_date_fake_year, year);
//noinspection unchecked
preferences.setIntegers(dayPair, monthPair, yearPair);
}
public void setEnabled(boolean newValue) {
preferences.setBoolean(R.string.key_debug_daily_reminder_date_enable, newValue);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/DailyReminderJob.kt
================================================
package com.alexstyl.specialdates.dailyreminder
import com.evernote.android.job.DailyJob
class DailyReminderJob(private val presenter: DailyReminderPresenter,
private val notifier: DailyReminderNotifier) : DailyJob() {
companion object {
const val TAG = "Daily_reminder"
}
override fun onRunDailyJob(params: Params): DailyJobResult {
val view = NotificationDailyReminderView(notifier)
presenter.startPresentingInto(view)
presenter.stopPresenting()
return DailyJobResult.SUCCESS
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/DailyReminderModule.java
================================================
package com.alexstyl.specialdates.dailyreminder;
import android.app.NotificationManager;
import android.content.Context;
import com.alexstyl.Logger;
import com.alexstyl.resources.Colors;
import com.alexstyl.specialdates.CrashAndErrorTracker;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.Strings;
import com.alexstyl.specialdates.date.Date;
import com.alexstyl.specialdates.events.bankholidays.BankHolidayProvider;
import com.alexstyl.specialdates.events.bankholidays.BankHolidaysUserSettings;
import com.alexstyl.specialdates.events.namedays.NamedayUserSettings;
import com.alexstyl.specialdates.events.namedays.calendar.resource.NamedayCalendarProvider;
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsProvider;
import com.alexstyl.specialdates.images.ImageLoader;
import com.alexstyl.specialdates.permissions.MementoPermissions;
import com.alexstyl.specialdates.settings.DailyReminderNavigator;
import dagger.Module;
import dagger.Provides;
@Module
public class DailyReminderModule {
@Provides
DailyReminderUserSettings settings(Context context) {
EasyPreferences defaultPreferences = EasyPreferences.createForDefaultPreferences(context);
return new DailyReminderPreferences(defaultPreferences);
}
@Provides
DailyReminderViewModelFactory factory(Strings strings, Colors colors) {
return new AndroidDailyReminderViewModelFactory(strings, Date.Companion.today(), colors);
}
@Provides
DailyReminderPresenter presenter(MementoPermissions permissions,
PeopleEventsProvider peopleEventsProvider,
NamedayUserSettings namedaySettings,
BankHolidaysUserSettings bankholidaySettings,
NamedayCalendarProvider namedayCalendarProvider,
DailyReminderViewModelFactory factory,
CrashAndErrorTracker errorTracker,
BankHolidayProvider bankHolidayProvider) {
return new DailyReminderPresenter(
permissions,
peopleEventsProvider,
namedaySettings,
bankholidaySettings,
namedayCalendarProvider,
factory,
errorTracker,
bankHolidayProvider
);
}
@Provides
DailyReminderNotifier notifier(Context context,
ImageLoader imageLoader,
Colors colors,
NotificationManager notificationManager) {
return new AndroidDailyReminderNotifier(context, notificationManager, imageLoader, colors);
}
@Provides
DailyReminderOreoChannelCreator channelCreator(NotificationManager notificationManager,
Strings strings,
Logger logger,
DailyReminderUserSettings preferences) {
return new DailyReminderOreoChannelCreator(notificationManager, strings, preferences, logger);
}
@Provides
DailyReminderNavigator navigator() {
return new DailyReminderNavigator();
}
@Provides
DailyReminderScheduler scheduler() {
return new AndroidDailyReminderScheduler();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/DailyReminderOreoChannelCreator.kt
================================================
package com.alexstyl.specialdates.dailyreminder
import android.annotation.TargetApi
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.graphics.Color
import android.media.AudioAttributes
import android.os.Build
import com.alexstyl.Logger
import com.alexstyl.android.Version
import com.alexstyl.android.toUri
import com.alexstyl.specialdates.Strings
@TargetApi(Build.VERSION_CODES.O)
class DailyReminderOreoChannelCreator(private val notificationManager: NotificationManager,
private val strings: Strings,
private val dailyReminderPreferences: DailyReminderUserSettings,
private val logger: Logger) {
fun createDailyReminderChannel() {
if (!Version.hasOreo()) {
return
}
val group = NotificationChannelGroup(NotificationConstants.DAILY_REMINDER_GROUP_ID, strings.dailyReminder())
if (notificationManager.notificationChannelGroups.contains(group)) {
logger.warning("Already contains Group '${group.name}'. Won't create new channels [$group]")
return
}
notificationManager.createNotificationChannelGroup(group)
createContactsChannel()
createNamedayChannel()
createBankHolidayChannel()
}
private fun createContactsChannel() {
val contactsChannel = NotificationChannel(
NotificationConstants.CHANNEL_ID_CONTACTS,
strings.contacts(),
NotificationManager.IMPORTANCE_DEFAULT)
contactsChannel.group = NotificationConstants.DAILY_REMINDER_GROUP_ID
contactsChannel.enableLights(true)
contactsChannel.lightColor = Color.RED
contactsChannel.enableVibration(dailyReminderPreferences.isVibrationEnabled())
contactsChannel.setSound(dailyReminderPreferences.getRingtone().toUri(), AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build())
notificationManager.createNotificationChannel(contactsChannel)
}
private fun createNamedayChannel() {
val namedaysChannel = NotificationChannel(
NotificationConstants.CHANNEL_ID_NAMEDAYS,
strings.namedays(),
NotificationManager.IMPORTANCE_LOW)
namedaysChannel.group = NotificationConstants.DAILY_REMINDER_GROUP_ID
namedaysChannel.enableLights(false)
namedaysChannel.enableVibration(dailyReminderPreferences.isVibrationEnabled())
namedaysChannel.setSound(dailyReminderPreferences.getRingtone().toUri(), AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build())
notificationManager.createNotificationChannel(namedaysChannel)
}
private fun createBankHolidayChannel() {
val bankHolidaysChannel = NotificationChannel(
NotificationConstants.CHANNEL_ID_BANKHOLIDAY,
strings.bankholidays(),
NotificationManager.IMPORTANCE_LOW)
bankHolidaysChannel.group = NotificationConstants.DAILY_REMINDER_GROUP_ID
bankHolidaysChannel.enableLights(false)
bankHolidaysChannel.enableVibration(dailyReminderPreferences.isVibrationEnabled())
bankHolidaysChannel.setSound(dailyReminderPreferences.getRingtone().toUri(), AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build())
notificationManager.createNotificationChannel(bankHolidaysChannel)
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/DailyReminderPreferences.kt
================================================
package com.alexstyl.specialdates.dailyreminder
import android.media.RingtoneManager
import android.net.Uri
import com.alexstyl.specialdates.EasyPreferences
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.TimeOfDay
import java.net.URI
class DailyReminderPreferences(private val preferences: EasyPreferences)
: DailyReminderUserSettings {
override fun setEnabled(isEnabled: Boolean) {
preferences.setBoolean(R.string.key_daily_reminder, isEnabled)
}
override fun isEnabled(): Boolean = preferences.getBoolean(R.string.key_daily_reminder, true)
override fun getTimeSet(): TimeOfDay {
val time = preferences
.getString(R.string.key_daily_reminder_time, DEFAULT_DAILY_REMINDER_TIME)
.split(":")
return TimeOfDay(time[0].toInt(), time[1].toInt())
}
override fun getRingtone(): URI {
val selectedRingtone = preferences.getString(R.string.key_daily_reminder_ringtone, null)
return selectedRingtone?.toURI() ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION).toURI()
}
override fun isVibrationEnabled(): Boolean = preferences.getBoolean(R.string.key_daily_reminder_vibrate_enabled, false)
override fun setDailyReminderTime(time: TimeOfDay) {
preferences.setString(R.string.key_daily_reminder_time, time.toString())
}
private fun Uri.toURI(): URI = this.toString().toURI()
private fun String.toURI(): URI = URI.create(this)
companion object {
private const val DEFAULT_DAILY_REMINDER_TIME = "08:00"
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/NoActions.kt
================================================
package com.alexstyl.specialdates.dailyreminder
import com.alexstyl.specialdates.person.ContactActions
import java.net.URI
class NoActions : ContactActions {
override fun dial(phoneNumber: String) = {
// do nothing
}
override fun view(data: URI, mimetype: String) = {
// do nothing
}
override fun view(data: URI) = {
// do nothing
}
override fun message(phoneNumber: String) = {
// do nothing
}
override fun email(emailAdress: String) = {
// do nothing
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/NotificationConstants.kt
================================================
package com.alexstyl.specialdates.dailyreminder
import com.alexstyl.specialdates.BuildConfig
object NotificationConstants {
const val CHANNEL_ID_CONTACTS = BuildConfig.APPLICATION_ID + ".channel.contacts"
const val CHANNEL_ID_NAMEDAYS = BuildConfig.APPLICATION_ID + ".channel.namedays"
const val CHANNEL_ID_BANKHOLIDAY = BuildConfig.APPLICATION_ID + ".channel.bankholiday"
const val NOTIFICATION_ID_CONTACTS_SUMMARY: Int = 4001
const val NOTIFICATION_ID_NAMEDAYS = 4002
const val NOTIFICATION_ID_BANK_HOLIDAY = 4003
const val DAILY_REMINDER_GROUP_ID = BuildConfig.APPLICATION_ID + ".group.daily_reminder"
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/NotificationDailyReminderView.kt
================================================
package com.alexstyl.specialdates.dailyreminder
class NotificationDailyReminderView(private val notifier: DailyReminderNotifier)
: DailyReminderView {
override fun show(viewModel: DailyReminderViewModel) {
notifier.notifyFor(viewModel)
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/actions/AndroidContactActionsView.kt
================================================
package com.alexstyl.specialdates.dailyreminder.actions
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.person.ContactActionViewModel
import com.alexstyl.specialdates.person.ContactActionsAdapter
class AndroidContactActionsView(private val contact: Contact,
private val recyclerView: ContactActionsAdapter)
: ContactActionsView {
override fun display(viewModels: List) {
recyclerView.displayCallMethods(viewModels)
}
override fun contact(): Contact = contact
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/actions/ContactActionsModule.java
================================================
package com.alexstyl.specialdates.dailyreminder.actions;
import com.alexstyl.specialdates.CrashAndErrorTracker;
import com.alexstyl.specialdates.contact.ContactIntentExtractor;
import com.alexstyl.specialdates.contact.ContactsProvider;
import com.alexstyl.specialdates.person.ContactActionsProvider;
import dagger.Module;
import dagger.Provides;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@Module
public class ContactActionsModule {
@Provides
ContactActionsPresenter presenter(ContactActionsProvider personActionsProvider) {
return new ContactActionsPresenter(personActionsProvider, Schedulers.io(), AndroidSchedulers.mainThread());
}
@Provides
ContactIntentExtractor extractor(CrashAndErrorTracker tracker, ContactsProvider contactProvider) {
return new ContactIntentExtractor(tracker, contactProvider);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/actions/ContactActionsPresenter.kt
================================================
package com.alexstyl.specialdates.dailyreminder.actions
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.person.ContactActions
import com.alexstyl.specialdates.person.ContactActionsProvider
import io.reactivex.Observable
import io.reactivex.Scheduler
import io.reactivex.disposables.Disposable
class ContactActionsPresenter(
private val personActionsProvider: ContactActionsProvider,
private val workScheduler: Scheduler,
private val resultScheduler: Scheduler) {
private var disposable: Disposable? = null
fun startPresentingCallsInto(view: ContactActionsView, contactActions: ContactActions) {
disposable =
callActionsFor(contactActions, view.contact())
.observeOn(resultScheduler)
.subscribeOn(workScheduler)
.subscribe { viewModels ->
view.display(viewModels)
}
}
private fun callActionsFor(contactActions: ContactActions, contact: Contact) =
Observable.fromCallable { personActionsProvider.callActionsFor(contact, contactActions) }
fun startPresentingMessagingInto(view: ContactActionsView, contactActions: ContactActions) {
disposable =
messagingActionsFor(contactActions, view.contact())
.observeOn(resultScheduler)
.subscribeOn(workScheduler)
.subscribe { viewModels ->
view.display(viewModels)
}
}
private fun messagingActionsFor(contactActions: ContactActions, contact: Contact) =
Observable.fromCallable { personActionsProvider.messagingActionsFor(contact, contactActions) }
fun stopPresenting() {
disposable?.dispose()
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/actions/ContactActionsView.kt
================================================
package com.alexstyl.specialdates.dailyreminder.actions
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.person.ContactActionViewModel
interface ContactActionsView {
fun display(viewModels: List)
fun contact(): Contact
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/dailyreminder/actions/PersonActionsActivity.kt
================================================
package com.alexstyl.specialdates.dailyreminder.actions
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_CALL
import android.content.Intent.ACTION_SENDTO
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.MementoApplication
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.contact.ContactIntentExtractor
import com.alexstyl.specialdates.person.AndroidContactActions
import com.alexstyl.specialdates.person.ContactActionsAdapter
import com.alexstyl.specialdates.ui.base.ThemedMementoActivity
import javax.inject.Inject
class PersonActionsActivity : ThemedMementoActivity() {
var presenter: ContactActionsPresenter? = null
@Inject set
var extractor: ContactIntentExtractor? = null
@Inject set
var errorTracker: CrashAndErrorTracker? = null
@Inject set
var view: ContactActionsView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_call)
(application as MementoApplication).applicationModule.inject(this)
val recyclerView = findViewById(R.id.actions_list)!!
recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
val adapter = ContactActionsAdapter {
it.action.run()
finish()
}
recyclerView.adapter = adapter
val contact = extractor?.getContactExtra(intent)
if (contact != null && contact.isPresent) {
view = AndroidContactActionsView(contact.get(), adapter)
} else {
errorTracker?.track(RuntimeException("Tried to load the actions for a contact from $intent"))
finish()
}
}
override fun onStart() {
super.onStart()
if (view != null) {
startPresentingInto(view!!)
}
}
private fun startPresentingInto(view: ContactActionsView) {
val action = AndroidContactActions(this)
when {
intent.action == ACTION_CALL -> presenter?.startPresentingCallsInto(view, action)
intent.action == ACTION_SENDTO -> presenter?.startPresentingMessagingInto(view, action)
else -> {
}
}
}
override fun onStop() {
super.onStop()
presenter?.stopPresenting()
}
companion object {
fun buildCallIntentFor(context: Context, contact: Contact): Intent {
return Intent(context, PersonActionsActivity::class.java)
.setAction(ACTION_CALL)
.putContactExtra(contact)
}
fun buildSendIntentFor(context: Context, contact: Contact): Intent {
return Intent(context, PersonActionsActivity::class.java)
.setAction(ACTION_SENDTO)
.putContactExtra(contact)
}
private fun Intent.putContactExtra(contact: Contact): Intent {
return putExtra(ContactIntentExtractor.EXTRA_CONTACT_ID, contact.contactID)
.putExtra(ContactIntentExtractor.EXTRA_CONTACT_SOURCE, contact.source)
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/date/AndroidDateLabelCreator.kt
================================================
package com.alexstyl.specialdates.date
import android.content.Context
import android.text.format.DateUtils
class AndroidDateLabelCreator(private val context: Context) : DateLabelCreator {
override fun createLabelWithoutYear(date: Date): String {
val formatFlags = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR
return DateUtils.formatDateTime(context, date.toMillis(), formatFlags)
}
override fun createWithYearPreferred(date: Date): String {
return if (date.hasYear()) {
val formatFlags = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR
DateUtils.formatDateTime(context, date.toMillis(), formatFlags)
} else {
val formatFlags = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR
DateUtils.formatDateTime(context, date.toMillis(), formatFlags)
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/date/DateModule.java
================================================
package com.alexstyl.specialdates.date;
import android.content.Context;
import com.alexstyl.specialdates.CrashAndErrorTracker;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
@Singleton
public class DateModule {
@Provides
@Singleton
DateLabelCreator labelCreator(Context context) {
return new AndroidDateLabelCreator(context);
}
@Provides
DateParser dateParser(CrashAndErrorTracker errorTracker) {
return new DateParser(errorTracker);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/AndroidDonation.java
================================================
package com.alexstyl.specialdates.donate;
enum AndroidDonation implements Donation {
SKU_DONATE_1("1€", "donate_1"),
SKU_DONATE_2("3€", "donate_2"),
SKU_DONATE_3("5€", "donate_3"),
SKU_DONATE_4("8€", "donate_4"),
SKU_DONATE_5("10€", "donate_5"),
SKU_DONATE_6("15€", "donate_6"),
SKU_DONATE_7("20€", "donate_7");
private final String priceLabel;
private final String identifier;
AndroidDonation(String priceLabel, String identifier) {
this.priceLabel = priceLabel;
this.identifier = identifier;
}
@Override
public String getAmount() {
return priceLabel;
}
@Override
public String getIdentifier() {
return identifier;
}
public static Donation valueOfIndex(int index) {
return values()[index];
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/AndroidDonationConstants.java
================================================
package com.alexstyl.specialdates.donate;
public final class AndroidDonationConstants {
private AndroidDonationConstants() {
// hide this
}
public static final String PUBLIC_KEY =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsYY0f8jTzL1RkaxI6RgasYZ1anbjHo3uOrSecriRJdQtTtiynmnjSqg0mh79gJunqbqtJtQQaFYo"
+ "wm1D5uTWkZ30CmZqrKAz8PILbF7andUNECZRuUENfsptHhaBQHYcDucXLP2QDuerEaYPcBW4kQoyv4Jyfm/"
+ "vDou04VwMADcFQ4vZ/Rj6tvt+KNXvMbJorQslDSmA3Ul+oDqDJ1K0TFPFOv3ECjuw+J/g0TX6yAcS9LR8xHVppWE9fW0+qPWd2tT"
+ "o0CIb3W3h+lgREkDZEGRlWvGijWmND7qFbhCv2jURen851trNSLIvyOQwraCVku6VkxSxwCeS2E26q7B5mQIDAQAB";
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/AndroidDonationService.java
================================================
package com.alexstyl.specialdates.donate;
import android.app.Activity;
import android.widget.Toast;
import com.alexstyl.specialdates.CrashAndErrorTracker;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.analytics.Analytics;
import com.alexstyl.specialdates.donate.util.IabHelper;
import com.alexstyl.specialdates.donate.util.IabResult;
import com.alexstyl.specialdates.donate.util.Inventory;
import com.alexstyl.specialdates.donate.util.Purchase;
public class AndroidDonationService implements DonationService {
private final IabHelper iabHelper;
private final Activity activity;
private final DonationPreferences donationPreferences;
private final Analytics analytics;
private final CrashAndErrorTracker tracker;
private final DonateMonitor monitor;
private DonationCallbacks listener;
public AndroidDonationService(IabHelper iabHelper,
Activity activity,
DonationPreferences donationPreferences,
Analytics analytics,
CrashAndErrorTracker tracker,
DonateMonitor monitor) {
this.iabHelper = iabHelper;
this.activity = activity;
this.donationPreferences = donationPreferences;
this.analytics = analytics;
this.tracker = tracker;
this.monitor = monitor;
}
@Override
public void setup(final DonationCallbacks listener) {
this.listener = listener;
iabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
@Override
public void onIabSetupFinished(IabResult result) {
if (result.isFailure()) {
listener.onDonateException(result.getMessage());
}
}
});
}
@Override
public void placeDonation(final Donation donation, int requestCode) {
try {
iabHelper.launchPurchaseFlow(
activity, donation.getIdentifier(), requestCode, new IabHelper.OnIabPurchaseFinishedListener() {
@Override
public void onIabPurchaseFinished(IabResult result, Purchase info) {
if (result.isSuccess()) {
analytics.trackDonationPlaced(donation);
listener.onDonationFinished(donation);
donationPreferences.markAsDonated();
}
}
}
);
} catch (IabHelper.IabAsyncInProgressException e) {
tracker.track(e);
listener.onDonateException(e.getMessage());
}
}
@Override
public void dispose() {
try {
iabHelper.dispose();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void restoreDonations() {
try {
iabHelper.queryInventoryAsync(new IabHelper.QueryInventoryFinishedListener() {
@Override
public void onQueryInventoryFinished(IabResult result, Inventory inv) {
boolean hasDonated = containsDonations(inv);
if (hasDonated) {
Toast.makeText(activity, R.string.donate_thanks_for_donating, Toast.LENGTH_SHORT).show();
donationPreferences.markAsDonated();
monitor.onDonationUpdated();
analytics.trackDonationRestored();
} else {
Toast.makeText(activity, R.string.donate_no_donation_found, Toast.LENGTH_SHORT).show();
}
}
});
} catch (IabHelper.IabAsyncInProgressException e) {
tracker.track(e);
}
}
private static boolean containsDonations(Inventory inventory) {
for (AndroidDonation donation : AndroidDonation.values()) {
if (inventory.hasPurchase(donation.getIdentifier())) {
return true;
}
}
return false;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/DonateActivity.java
================================================
package com.alexstyl.specialdates.donate;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.widget.NestedScrollView;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.Toast;
import com.alexstyl.android.Version;
import com.alexstyl.specialdates.AppComponent;
import com.alexstyl.specialdates.CrashAndErrorTracker;
import com.alexstyl.specialdates.MementoApplication;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.Strings;
import com.alexstyl.specialdates.TextViewLabelSetter;
import com.alexstyl.specialdates.analytics.Analytics;
import com.alexstyl.specialdates.donate.util.IabHelper;
import com.alexstyl.specialdates.images.ImageLoader;
import com.alexstyl.specialdates.ui.LolipopHideStatusBarListener;
import com.alexstyl.specialdates.ui.base.MementoActivity;
import com.novoda.notils.caster.Views;
import javax.inject.Inject;
import java.net.URI;
public class DonateActivity extends MementoActivity {
private static final int REQUEST_CODE = 1004;
private static final int SCROLL_DOWN_ANIMATION_DELAY = 2000;
private static final URI DEV_IMAGE_URI = URI.create("http://alexstyl.com/memento-calendar/dev.jpg");
private static final int VELOCITY_Y = 50;
private DonatePresenter donatePresenter;
private SeekBar donateBar;
private CoordinatorLayout coordinator;
@Inject Analytics analytics;
@Inject Strings strings;
@Inject ImageLoader imageLoader;
@Inject IabHelper iabHelper;
@Inject DonationPreferences donationPreferences;
@Inject CrashAndErrorTracker tracker;
@Inject DonateMonitor monitor;
@Inject DonateMonitor donateMonitor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_donate);
AppComponent applicationModule = ((MementoApplication) getApplication()).getApplicationModule();
applicationModule.inject(this);
Toolbar toolbar = Views.findById(this, R.id.toolbar);
setSupportActionBar(toolbar);
coordinator = Views.findById(this, R.id.donate_coordinator);
ImageView avatar = Views.findById(this, R.id.donate_avatar);
imageLoader
.load(DEV_IMAGE_URI)
.into(avatar);
final AppBarLayout appBarLayout = Views.findById(this, R.id.app_bar_layout);
final NestedScrollView scrollView = Views.findById(this, R.id.scroll);
if (Version.INSTANCE.hasLollipop()) {
appBarLayout.addOnOffsetChangedListener(new LolipopHideStatusBarListener(getWindow()));
}
DonationService donationService = new AndroidDonationService(iabHelper, this, donationPreferences, analytics, tracker, donateMonitor);
final Button donateButton = Views.findById(this, R.id.donate_place_donation);
donateButton.requestFocus();
donatePresenter = new DonatePresenter(analytics, donationService, new TextViewLabelSetter(donateButton), strings);
donateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Donation donation = AndroidDonation.valueOfIndex(donateBar.getProgress());
donatePresenter.placeDonation(donation, REQUEST_CODE);
}
});
setupDonateBar();
donatePresenter.startPresenting(donationCallbacks());
scrollView.postDelayed(new Runnable() {
@Override
public void run() {
scrollToDonate();
}
private void scrollToDonate() {
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior();
behavior.onNestedFling(coordinator, appBarLayout, null, 0, VELOCITY_Y, true);
}
}, SCROLL_DOWN_ANIMATION_DELAY);
}
private void setupDonateBar() {
donateBar = Views.findById(this, R.id.donation_bar);
donateBar.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
String amount = AndroidDonation.valueOfIndex(progress).getAmount();
donatePresenter.displaySelectedDonation(amount);
}
});
AndroidDonation[] values = AndroidDonation.values();
donateBar.setProgress(1);
donateBar.setMax(values.length - 1);
}
@Override
public void onDestroy() {
super.onDestroy();
donatePresenter.stopPresenting();
}
private DonationCallbacks donationCallbacks() {
return new DonationCallbacks() {
@Override
public void onDonateException(String message) {
tracker.track(new RuntimeException(message));
finish();
}
@Override
public void onDonationFinished(Donation donation) {
monitor.onDonationUpdated();
Toast.makeText(DonateActivity.this, R.string.donate_thanks_for_donating, Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
}
};
}
public static Intent createIntent(Context context) {
return new Intent(context, DonateActivity.class);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/DonateModule.java
================================================
package com.alexstyl.specialdates.donate;
import android.content.Context;
import com.alexstyl.specialdates.donate.util.IabHelper;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class DonateModule {
@Provides
IabHelper providesIabHelper(Context context) {
return new IabHelper(context, AndroidDonationConstants.PUBLIC_KEY);
}
@Provides
DonationPreferences providesDonationPreferences(Context context) {
return DonationPreferences.newInstance(context);
}
@Provides
@Singleton
DonateMonitor providesMonitor(){
return new DonateMonitor();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/DonatePresenter.java
================================================
package com.alexstyl.specialdates.donate;
import com.alexstyl.specialdates.Strings;
import com.alexstyl.specialdates.LabelSetter;
import com.alexstyl.specialdates.analytics.Analytics;
import com.alexstyl.specialdates.analytics.Screen;
class DonatePresenter {
private final Analytics analytics;
private final DonationService donationService;
private final LabelSetter donateButtonLabel;
private final Strings strings;
DonatePresenter(Analytics analytics,
DonationService donationService,
LabelSetter donateButtonLabel,
Strings strings) {
this.analytics = analytics;
this.donationService = donationService;
this.donateButtonLabel = donateButtonLabel;
this.strings = strings;
}
void displaySelectedDonation(String amount) {
donateButtonLabel.setLabel(strings.donateAmount(amount));
}
void startPresenting(DonationCallbacks donationCallbacks) {
analytics.trackScreen(Screen.DONATE);
donationService.setup(donationCallbacks);
}
void placeDonation(Donation donation, int requestCode) {
donationService.placeDonation(donation, requestCode);
analytics.trackDonationStarted(donation);
}
void stopPresenting() {
donationService.dispose();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/DonationPreferences.java
================================================
package com.alexstyl.specialdates.donate;
import android.content.Context;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.R;
public final class DonationPreferences {
private final EasyPreferences preferences;
public static DonationPreferences newInstance(Context context) {
EasyPreferences preferences = EasyPreferences.createForDefaultPreferences(context);
return new DonationPreferences(preferences);
}
private DonationPreferences(EasyPreferences preferences) {
this.preferences = preferences;
}
void markAsDonated() {
preferences.setBoolean(R.string.key_has_donated, true);
}
public boolean hasDonated() {
return preferences.getBoolean(R.string.key_has_donated, false);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/SimpleOnSeekBarChangeListener.java
================================================
package com.alexstyl.specialdates.donate;
import android.widget.SeekBar;
class SimpleOnSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// do nothing
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// do nothing
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// do nothing
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/util/IabBroadcastReceiver.java
================================================
/* Copyright (c) 2014 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alexstyl.specialdates.donate.util;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* Receiver for the "com.android.vending.billing.PURCHASES_UPDATED" Action
* from the Play Store.
*
*
It is possible that an in-app item may be acquired without the
* application calling getBuyIntent(), for example if the item can be
* redeemed from inside the Play Store using a promotional code. If this
* application isn't running at the time, then when it is started a call
* to getPurchases() will be sufficient notification. However, if the
* application is already running in the background when the item is acquired,
* a message to this BroadcastReceiver will indicate that the an item
* has been acquired.
*/
public class IabBroadcastReceiver extends BroadcastReceiver {
/**
* DonationCallbacks interface for received broadcast messages.
*/
public interface IabBroadcastListener {
void receivedBroadcast();
}
/**
* The Intent action that this Receiver should filter for.
*/
public static final String ACTION = "com.android.vending.billing.PURCHASES_UPDATED";
private final IabBroadcastListener mListener;
public IabBroadcastReceiver(IabBroadcastListener listener) {
mListener = listener;
}
@Override
public void onReceive(Context context, Intent intent) {
if (mListener != null) {
mListener.receivedBroadcast();
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/util/IabException.java
================================================
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alexstyl.specialdates.donate.util;
/**
* Exception thrown when something went wrong with in-app billing.
* An IabException has an associated IabResult (an error).
* To get the IAB result that caused this exception to be thrown,
* call {@link #getResult()}.
*/
public class IabException extends Exception {
IabResult mResult;
public IabException(IabResult r) {
this(r, null);
}
public IabException(int response, String message) {
this(new IabResult(response, message));
}
public IabException(IabResult r, Exception cause) {
super(r.getMessage(), cause);
mResult = r;
}
public IabException(int response, String message, Exception cause) {
this(new IabResult(response, message), cause);
}
/** Returns the IAB result (error) that this exception signals. */
public IabResult getResult() { return mResult; }
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/util/IabHelper.java
================================================
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alexstyl.specialdates.donate.util;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.content.ServiceConnection;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import com.android.vending.billing.IInAppBillingService;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONException;
/**
* Provides convenience methods for in-app billing. You can create one instance of this
* class for your application and use it to process in-app billing operations.
* It provides synchronous (blocking) and asynchronous (non-blocking) methods for
* many common in-app billing operations, as well as automatic signature
* verification.
*
* After instantiating, you must perform setup in order to start using the object.
* To perform setup, call the {@link #startSetup} method and provide a listener;
* that listener will be notified when setup is complete, after which (and not before)
* you may call other methods.
*
* After setup is complete, you will typically want to request an inventory of owned
* items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
* and related methods.
*
* When you are done with this object, don't forget to call {@link #dispose}
* to ensure proper cleanup. This object holds a binding to the in-app billing
* service, which will leak unless you dispose of it correctly. If you created
* the object on an Activity's onCreate method, then the recommended
* place to dispose of it is the Activity's onDestroy method. It is invalid to
* dispose the object while an asynchronous operation is in progress. You can
* call {@link #disposeWhenFinished()} to ensure that any in-progress operation
* completes before the object is disposed.
*
* A note about threading: When using this object from a background thread, you may
* call the blocking versions of methods; when using from a UI thread, call
* only the asynchronous versions and handle the results via callbacks.
* Also, notice that you can only call one asynchronous operation at a time;
* attempting to start a second asynchronous operation while the first one
* has not yet completed will result in an exception being thrown.
*
*/
public class IabHelper {
// Is debug logging enabled?
boolean mDebugLog = false;
String mDebugTag = "IabHelper";
// Is setup done?
boolean mSetupDone = false;
// Has this object been disposed of? (If so, we should ignore callbacks, etc)
boolean mDisposed = false;
// Do we need to dispose this object after an in-progress asynchronous operation?
boolean mDisposeAfterAsync = false;
// Are subscriptions supported?
boolean mSubscriptionsSupported = false;
// Is subscription update supported?
boolean mSubscriptionUpdateSupported = false;
// Is an asynchronous operation in progress?
// (only one at a time can be in progress)
boolean mAsyncInProgress = false;
// Ensure atomic access to mAsyncInProgress and mDisposeAfterAsync.
private final Object mAsyncInProgressLock = new Object();
// (for logging/debugging)
// if mAsyncInProgress == true, what asynchronous operation is in progress?
String mAsyncOperation = "";
// Context we were passed during initialization
Context mContext;
// Connection to the service
IInAppBillingService mService;
ServiceConnection mServiceConn;
// The request code used to launch purchase flow
int mRequestCode;
// The item type of the current purchase flow
String mPurchasingItemType;
// Public key for verifying signature, in base64 encoding
String mSignatureBase64 = null;
// Billing response codes
public static final int BILLING_RESPONSE_RESULT_OK = 0;
public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
public static final int BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE = 2;
public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
// IAB Helper error codes
public static final int IABHELPER_ERROR_BASE = -1000;
public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
public static final int IABHELPER_BAD_RESPONSE = -1002;
public static final int IABHELPER_VERIFICATION_FAILED = -1003;
public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
public static final int IABHELPER_USER_CANCELLED = -1005;
public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
public static final int IABHELPER_MISSING_TOKEN = -1007;
public static final int IABHELPER_UNKNOWN_ERROR = -1008;
public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
public static final int IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE = -1011;
// Keys for the responses from InAppBillingService
public static final String RESPONSE_CODE = "RESPONSE_CODE";
public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
// Item types
public static final String ITEM_TYPE_INAPP = "inapp";
public static final String ITEM_TYPE_SUBS = "subs";
// some fields on the getSkuDetails response bundle
public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
/**
* Creates an instance. After creation, it will not yet be ready to use. You must perform
* setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
* block and is safe to call from a UI thread.
*
* @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
* @param base64PublicKey Your application's public key, encoded in base64.
* This is used for verification of purchase signatures. You can find your app's base64-encoded
* public key in your application's page on Google Play Developer Console. Note that this
* is NOT your "developer public key".
*/
public IabHelper(Context ctx, String base64PublicKey) {
mContext = ctx.getApplicationContext();
mSignatureBase64 = base64PublicKey;
logDebug("IAB helper created.");
}
/**
* Enables or disable debug logging through LogCat.
*/
public void enableDebugLogging(boolean enable, String tag) {
checkNotDisposed();
mDebugLog = enable;
mDebugTag = tag;
}
public void enableDebugLogging(boolean enable) {
checkNotDisposed();
mDebugLog = enable;
}
/**
* Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
* when the setup process is complete.
*/
public interface OnIabSetupFinishedListener {
/**
* Called to notify that setup is complete.
*
* @param result The result of the setup process.
*/
void onIabSetupFinished(IabResult result);
}
/**
* Starts the setup process. This will start up the setup process asynchronously.
* You will be notified through the listener when the setup process is complete.
* This method is safe to call from a UI thread.
*
* @param listener The listener to notify when the setup process is complete.
*/
public void startSetup(final OnIabSetupFinishedListener listener) {
// If already set up, can't do it again.
checkNotDisposed();
if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
// Connection to IAB service
logDebug("Starting in-app billing setup.");
mServiceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
logDebug("Billing service disconnected.");
mService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (mDisposed) return;
logDebug("Billing service connected.");
mService = IInAppBillingService.Stub.asInterface(service);
String packageName = mContext.getPackageName();
try {
logDebug("Checking for in-app billing 3 support.");
// check for in-app billing v3 support
int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
if (response != BILLING_RESPONSE_RESULT_OK) {
if (listener != null) listener.onIabSetupFinished(new IabResult(response,
"Error checking for billing v3 support."));
// if in-app purchases aren't supported, neither are subscriptions
mSubscriptionsSupported = false;
mSubscriptionUpdateSupported = false;
return;
} else {
logDebug("In-app billing version 3 supported for " + packageName);
}
// Check for v5 subscriptions support. This is needed for
// getBuyIntentToReplaceSku which allows for subscription update
response = mService.isBillingSupported(5, packageName, ITEM_TYPE_SUBS);
if (response == BILLING_RESPONSE_RESULT_OK) {
logDebug("Subscription re-signup AVAILABLE.");
mSubscriptionUpdateSupported = true;
} else {
logDebug("Subscription re-signup not available.");
mSubscriptionUpdateSupported = false;
}
if (mSubscriptionUpdateSupported) {
mSubscriptionsSupported = true;
} else {
// check for v3 subscriptions support
response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
if (response == BILLING_RESPONSE_RESULT_OK) {
logDebug("Subscriptions AVAILABLE.");
mSubscriptionsSupported = true;
} else {
logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
mSubscriptionsSupported = false;
mSubscriptionUpdateSupported = false;
}
}
mSetupDone = true;
}
catch (RemoteException e) {
if (listener != null) {
listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
"RemoteException while setting up in-app billing."));
}
e.printStackTrace();
return;
}
if (listener != null) {
listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
}
}
};
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
serviceIntent.setPackage("com.android.vending");
List intentServices = mContext.getPackageManager().queryIntentServices(serviceIntent, 0);
if (intentServices != null && !intentServices.isEmpty()) {
// service available to handle that Intent
mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
}
else {
// no service available to handle that Intent
if (listener != null) {
listener.onIabSetupFinished(
new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
"Billing service unavailable on device."));
}
}
}
/**
* Dispose of object, releasing resources. It's very important to call this
* method when you are done with this object. It will release any resources
* used by it such as service connections. Naturally, once the object is
* disposed of, it can't be used again.
*/
public void dispose() throws IabAsyncInProgressException {
synchronized (mAsyncInProgressLock) {
if (mAsyncInProgress) {
throw new IabAsyncInProgressException("Can't dispose because an async operation " +
"(" + mAsyncOperation + ") is in progress.");
}
}
logDebug("Disposing.");
mSetupDone = false;
if (mServiceConn != null) {
logDebug("Unbinding from service.");
if (mContext != null) mContext.unbindService(mServiceConn);
}
mDisposed = true;
mContext = null;
mServiceConn = null;
mService = null;
mPurchaseListener = null;
}
/**
* Disposes of object, releasing resources. If there is an in-progress async operation, this
* method will queue the dispose to occur after the operation has finished.
*/
public void disposeWhenFinished() {
synchronized (mAsyncInProgressLock) {
if (mAsyncInProgress) {
logDebug("Will dispose after async operation finishes.");
mDisposeAfterAsync = true;
} else {
try {
dispose();
} catch (IabAsyncInProgressException e) {
// Should never be thrown, because we call dispose() only after checking that
// there's not already an async operation in progress.
}
}
}
}
private void checkNotDisposed() {
if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
}
/** Returns whether subscriptions are supported. */
public boolean subscriptionsSupported() {
checkNotDisposed();
return mSubscriptionsSupported;
}
/**
* Callback that notifies when a purchase is finished.
*/
public interface OnIabPurchaseFinishedListener {
/**
* Called to notify that an in-app purchase finished. If the purchase was successful,
* then the sku parameter specifies which item was purchased. If the purchase failed,
* the sku and extraData parameters may or may not be null, depending on how far the purchase
* process went.
*
* @param result The result of the purchase.
* @param info The purchase information (null if purchase failed)
*/
void onIabPurchaseFinished(IabResult result, Purchase info);
}
// The listener registered on launchPurchaseFlow, which we have to call back when
// the purchase finishes
OnIabPurchaseFinishedListener mPurchaseListener;
public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener)
throws IabAsyncInProgressException {
launchPurchaseFlow(act, sku, requestCode, listener, "");
}
public void launchPurchaseFlow(Activity act, String sku, int requestCode,
OnIabPurchaseFinishedListener listener, String extraData)
throws IabAsyncInProgressException {
launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, null, requestCode, listener, extraData);
}
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
OnIabPurchaseFinishedListener listener) throws IabAsyncInProgressException {
launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
}
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
OnIabPurchaseFinishedListener listener, String extraData)
throws IabAsyncInProgressException {
launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, null, requestCode, listener, extraData);
}
/**
* Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
* which will involve bringing up the Google Play screen. The calling activity will be paused
* while the user interacts with Google Play, and the result will be delivered via the
* activity's {@link Activity#onActivityResult} method, at which point you must call
* this object's {@link #handleActivityResult} method to continue the purchase flow. This method
* MUST be called from the UI thread of the Activity.
*
* @param act The calling activity.
* @param sku The sku of the item to purchase.
* @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or
* ITEM_TYPE_SUBS)
* @param oldSkus A list of SKUs which the new SKU is replacing or null if there are none
* @param requestCode A request code (to differentiate from other responses -- as in
* {@link Activity#startActivityForResult}).
* @param listener The listener to notify when the purchase process finishes
* @param extraData Extra data (developer payload), which will be returned with the purchase
* data when the purchase completes. This extra data will be permanently bound to that
* purchase and will always be returned when the purchase is queried.
*/
public void launchPurchaseFlow(Activity act, String sku, String itemType, List oldSkus,
int requestCode, OnIabPurchaseFinishedListener listener, String extraData)
throws IabAsyncInProgressException {
checkNotDisposed();
checkSetupDone("launchPurchaseFlow");
flagStartAsync("launchPurchaseFlow");
IabResult result;
if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
"Subscriptions are not available.");
flagEndAsync();
if (listener != null) listener.onIabPurchaseFinished(r, null);
return;
}
try {
logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
Bundle buyIntentBundle;
if (oldSkus == null || oldSkus.isEmpty()) {
// Purchasing a new item or subscription re-signup
buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType,
extraData);
} else {
// Subscription upgrade/downgrade
if (!mSubscriptionUpdateSupported) {
IabResult r = new IabResult(IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE,
"Subscription updates are not available.");
flagEndAsync();
if (listener != null) listener.onIabPurchaseFinished(r, null);
return;
}
buyIntentBundle = mService.getBuyIntentToReplaceSkus(5, mContext.getPackageName(),
oldSkus, sku, itemType, extraData);
}
int response = getResponseCodeFromBundle(buyIntentBundle);
if (response != BILLING_RESPONSE_RESULT_OK) {
logError("Unable to buy item, Error response: " + getResponseDesc(response));
flagEndAsync();
result = new IabResult(response, "Unable to buy item");
if (listener != null) listener.onIabPurchaseFinished(result, null);
return;
}
PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
mRequestCode = requestCode;
mPurchaseListener = listener;
mPurchasingItemType = itemType;
act.startIntentSenderForResult(pendingIntent.getIntentSender(),
requestCode, new Intent(),
Integer.valueOf(0), Integer.valueOf(0),
Integer.valueOf(0));
}
catch (SendIntentException e) {
logError("SendIntentException while launching purchase flow for sku " + sku);
e.printStackTrace();
flagEndAsync();
result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
if (listener != null) listener.onIabPurchaseFinished(result, null);
}
catch (RemoteException e) {
logError("RemoteException while launching purchase flow for sku " + sku);
e.printStackTrace();
flagEndAsync();
result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
if (listener != null) listener.onIabPurchaseFinished(result, null);
}
}
/**
* Handles an activity result that's part of the purchase flow in in-app billing. If you
* are calling {@link #launchPurchaseFlow}, then you must call this method from your
* Activity's {@link Activity@onActivityResult} method. This method
* MUST be called from the UI thread of the Activity.
*
* @param requestCode The requestCode as you received it.
* @param resultCode The resultCode as you received it.
* @param data The data (Intent) as you received it.
* @return Returns true if the result was related to a purchase flow and was handled;
* false if the result was not related to a purchase, in which case you should
* handle it normally.
*/
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
IabResult result;
if (requestCode != mRequestCode) return false;
checkNotDisposed();
checkSetupDone("handleActivityResult");
// end of async purchase operation that started on launchPurchaseFlow
flagEndAsync();
if (data == null) {
logError("Null data in IAB activity result.");
result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
return true;
}
int responseCode = getResponseCodeFromIntent(data);
String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
logDebug("Successful resultcode from purchase activity.");
logDebug("Purchase data: " + purchaseData);
logDebug("Data signature: " + dataSignature);
logDebug("Extras: " + data.getExtras());
logDebug("Expected item type: " + mPurchasingItemType);
if (purchaseData == null || dataSignature == null) {
logError("BUG: either purchaseData or dataSignature is null.");
logDebug("Extras: " + data.getExtras().toString());
result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
return true;
}
Purchase purchase = null;
try {
purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
String sku = purchase.getSku();
// Verify signature
if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
logError("Purchase signature verification FAILED for sku " + sku);
result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
return true;
}
logDebug("Purchase signature successfully verified.");
}
catch (JSONException e) {
logError("Failed to parse purchase data.");
e.printStackTrace();
result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
return true;
}
if (mPurchaseListener != null) {
mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
}
}
else if (resultCode == Activity.RESULT_OK) {
// result code was OK, but in-app billing response was not OK.
logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
if (mPurchaseListener != null) {
result = new IabResult(responseCode, "Problem purchashing item.");
mPurchaseListener.onIabPurchaseFinished(result, null);
}
}
else if (resultCode == Activity.RESULT_CANCELED) {
logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
}
else {
logError("Purchase failed. Result code: " + Integer.toString(resultCode)
+ ". Response: " + getResponseDesc(responseCode));
result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
}
return true;
}
public Inventory queryInventory() throws IabException {
return queryInventory(false, null, null);
}
/**
* Queries the inventory. This will query all owned items from the server, as well as
* information on additional skus, if specified. This method may block or take long to execute.
* Do not call from a UI thread. For that, use the non-blocking version {@link #queryInventoryAsync}.
*
* @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
* as purchase information.
* @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
* Ignored if null or if querySkuDetails is false.
* @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
* Ignored if null or if querySkuDetails is false.
* @throws IabException if a problem occurs while refreshing the inventory.
*/
public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus,
List moreSubsSkus) throws IabException {
checkNotDisposed();
checkSetupDone("queryInventory");
try {
Inventory inv = new Inventory();
int r = queryPurchases(inv, ITEM_TYPE_INAPP);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying owned items).");
}
if (querySkuDetails) {
r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying prices of items).");
}
}
// if subscriptions are supported, then also query for subscriptions
if (mSubscriptionsSupported) {
r = queryPurchases(inv, ITEM_TYPE_SUBS);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
}
if (querySkuDetails) {
r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreSubsSkus);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
}
}
}
return inv;
}
catch (RemoteException e) {
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
}
catch (JSONException e) {
throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
}
}
/**
* DonationCallbacks that notifies when an inventory query operation completes.
*/
public interface QueryInventoryFinishedListener {
/**
* Called to notify that an inventory query operation completed.
*
* @param result The result of the operation.
* @param inv The inventory.
*/
void onQueryInventoryFinished(IabResult result, Inventory inv);
}
/**
* Asynchronous wrapper for inventory query. This will perform an inventory
* query as described in {@link #queryInventory}, but will do so asynchronously
* and call back the specified listener upon completion. This method is safe to
* call from a UI thread.
*
* @param querySkuDetails as in {@link #queryInventory}
* @param moreItemSkus as in {@link #queryInventory}
* @param moreSubsSkus as in {@link #queryInventory}
* @param listener The listener to notify when the refresh operation completes.
*/
public void queryInventoryAsync(final boolean querySkuDetails, final List moreItemSkus,
final List moreSubsSkus, final QueryInventoryFinishedListener listener)
throws IabAsyncInProgressException {
final Handler handler = new Handler();
checkNotDisposed();
checkSetupDone("queryInventory");
flagStartAsync("refresh inventory");
(new Thread(new Runnable() {
public void run() {
IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
Inventory inv = null;
try {
inv = queryInventory(querySkuDetails, moreItemSkus, moreSubsSkus);
}
catch (IabException ex) {
result = ex.getResult();
}
flagEndAsync();
final IabResult result_f = result;
final Inventory inv_f = inv;
if (!mDisposed && listener != null) {
handler.post(new Runnable() {
public void run() {
listener.onQueryInventoryFinished(result_f, inv_f);
}
});
}
}
})).start();
}
public void queryInventoryAsync(QueryInventoryFinishedListener listener)
throws IabAsyncInProgressException{
queryInventoryAsync(false, null, null, listener);
}
/**
* Consumes a given in-app product. Consuming can only be done on an item
* that's owned, and as a result of consumption, the user will no longer own it.
* This method may block or take long to return. Do not call from the UI thread.
* For that, see {@link #consumeAsync}.
*
* @param itemInfo The PurchaseInfo that represents the item to consume.
* @throws IabException if there is a problem during consumption.
*/
void consume(Purchase itemInfo) throws IabException {
checkNotDisposed();
checkSetupDone("consume");
if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
throw new IabException(IABHELPER_INVALID_CONSUMPTION,
"Items of type '" + itemInfo.mItemType + "' can't be consumed.");
}
try {
String token = itemInfo.getToken();
String sku = itemInfo.getSku();
if (token == null || token.equals("")) {
logError("Can't consume "+ sku + ". No token.");
throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ sku + " " + itemInfo);
}
logDebug("Consuming sku: " + sku + ", token: " + token);
int response = mService.consumePurchase(3, mContext.getPackageName(), token);
if (response == BILLING_RESPONSE_RESULT_OK) {
logDebug("Successfully consumed sku: " + sku);
}
else {
logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
throw new IabException(response, "Error consuming sku " + sku);
}
}
catch (RemoteException e) {
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
}
}
/**
* Callback that notifies when a consumption operation finishes.
*/
public interface OnConsumeFinishedListener {
/**
* Called to notify that a consumption has finished.
*
* @param purchase The purchase that was (or was to be) consumed.
* @param result The result of the consumption operation.
*/
void onConsumeFinished(Purchase purchase, IabResult result);
}
/**
* Callback that notifies when a multi-item consumption operation finishes.
*/
public interface OnConsumeMultiFinishedListener {
/**
* Called to notify that a consumption of multiple items has finished.
*
* @param purchases The purchases that were (or were to be) consumed.
* @param results The results of each consumption operation, corresponding to each
* sku.
*/
void onConsumeMultiFinished(List purchases, List results);
}
/**
* Asynchronous wrapper to item consumption. Works like {@link #consume}, but
* performs the consumption in the background and notifies completion through
* the provided listener. This method is safe to call from a UI thread.
*
* @param purchase The purchase to be consumed.
* @param listener The listener to notify when the consumption operation finishes.
*/
public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener)
throws IabAsyncInProgressException {
checkNotDisposed();
checkSetupDone("consume");
List purchases = new ArrayList();
purchases.add(purchase);
consumeAsyncInternal(purchases, listener, null);
}
/**
* Same as {@link #consumeAsync}, but for multiple items at once.
* @param purchases The list of PurchaseInfo objects representing the purchases to consume.
* @param listener The listener to notify when the consumption operation finishes.
*/
public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener)
throws IabAsyncInProgressException {
checkNotDisposed();
checkSetupDone("consume");
consumeAsyncInternal(purchases, null, listener);
}
/**
* Returns a human-readable description for the given response code.
*
* @param code The response code
* @return A human-readable string explaining the result code.
* It also includes the result code numerically.
*/
public static String getResponseDesc(int code) {
String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
"3:Billing Unavailable/4:Item unavailable/" +
"5:Developer Error/6:Error/7:Item Already Owned/" +
"8:Item not owned").split("/");
String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
"-1002:Bad response received/" +
"-1003:Purchase signature verification failed/" +
"-1004:Send intent failed/" +
"-1005:User cancelled/" +
"-1006:Unknown purchase response/" +
"-1007:Missing token/" +
"-1008:Unknown error/" +
"-1009:Subscriptions not available/" +
"-1010:Invalid consumption attempt").split("/");
if (code <= IABHELPER_ERROR_BASE) {
int index = IABHELPER_ERROR_BASE - code;
if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
else return String.valueOf(code) + ":Unknown IAB Helper Error";
}
else if (code < 0 || code >= iab_msgs.length)
return String.valueOf(code) + ":Unknown";
else
return iab_msgs[code];
}
// Checks that setup was done; if not, throws an exception.
void checkSetupDone(String operation) {
if (!mSetupDone) {
logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
}
}
// Workaround to bug where sometimes response codes come as Long instead of Integer
int getResponseCodeFromBundle(Bundle b) {
Object o = b.get(RESPONSE_CODE);
if (o == null) {
logDebug("Bundle with null response code, assuming OK (known issue)");
return BILLING_RESPONSE_RESULT_OK;
}
else if (o instanceof Integer) return ((Integer)o).intValue();
else if (o instanceof Long) return (int)((Long)o).longValue();
else {
logError("Unexpected type for bundle response code.");
logError(o.getClass().getName());
throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
}
}
// Workaround to bug where sometimes response codes come as Long instead of Integer
int getResponseCodeFromIntent(Intent i) {
Object o = i.getExtras().get(RESPONSE_CODE);
if (o == null) {
logError("Intent with no response code, assuming OK (known issue)");
return BILLING_RESPONSE_RESULT_OK;
}
else if (o instanceof Integer) return ((Integer)o).intValue();
else if (o instanceof Long) return (int)((Long)o).longValue();
else {
logError("Unexpected type for intent response code.");
logError(o.getClass().getName());
throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
}
}
void flagStartAsync(String operation) throws IabAsyncInProgressException {
synchronized (mAsyncInProgressLock) {
if (mAsyncInProgress) {
throw new IabAsyncInProgressException("Can't start async operation (" +
operation + ") because another async operation (" + mAsyncOperation +
") is in progress.");
}
mAsyncOperation = operation;
mAsyncInProgress = true;
logDebug("Starting async operation: " + operation);
}
}
void flagEndAsync() {
synchronized (mAsyncInProgressLock) {
logDebug("Ending async operation: " + mAsyncOperation);
mAsyncOperation = "";
mAsyncInProgress = false;
if (mDisposeAfterAsync) {
try {
dispose();
} catch (IabAsyncInProgressException e) {
// Should not be thrown, because we reset mAsyncInProgress immediately before
// calling dispose().
}
}
}
}
/**
* Exception thrown when the requested operation cannot be started because an async operation
* is still in progress.
*/
public static class IabAsyncInProgressException extends Exception {
public IabAsyncInProgressException(String message) {
super(message);
}
}
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
// Query purchases
logDebug("Querying owned items, item type: " + itemType);
logDebug("Package name: " + mContext.getPackageName());
boolean verificationFailed = false;
String continueToken = null;
do {
logDebug("Calling getPurchases with continuation token: " + continueToken);
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
itemType, continueToken);
int response = getResponseCodeFromBundle(ownedItems);
logDebug("Owned items response: " + String.valueOf(response));
if (response != BILLING_RESPONSE_RESULT_OK) {
logDebug("getPurchases() failed: " + getResponseDesc(response));
return response;
}
if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
|| !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
|| !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
logError("Bundle returned from getPurchases() doesn't contain required fields.");
return IABHELPER_BAD_RESPONSE;
}
ArrayList ownedSkus = ownedItems.getStringArrayList(
RESPONSE_INAPP_ITEM_LIST);
ArrayList purchaseDataList = ownedItems.getStringArrayList(
RESPONSE_INAPP_PURCHASE_DATA_LIST);
ArrayList signatureList = ownedItems.getStringArrayList(
RESPONSE_INAPP_SIGNATURE_LIST);
for (int i = 0; i < purchaseDataList.size(); ++i) {
String purchaseData = purchaseDataList.get(i);
String signature = signatureList.get(i);
String sku = ownedSkus.get(i);
if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
logDebug("Sku is owned: " + sku);
Purchase purchase = new Purchase(itemType, purchaseData, signature);
if (TextUtils.isEmpty(purchase.getToken())) {
logWarn("BUG: empty/null token!");
logDebug("Purchase data: " + purchaseData);
}
// Record ownership and token
inv.addPurchase(purchase);
}
else {
logWarn("Purchase signature verification **FAILED**. Not adding item.");
logDebug(" Purchase data: " + purchaseData);
logDebug(" Signature: " + signature);
verificationFailed = true;
}
}
continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
logDebug("Continuation token: " + continueToken);
} while (!TextUtils.isEmpty(continueToken));
return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
}
int querySkuDetails(String itemType, Inventory inv, List moreSkus)
throws RemoteException, JSONException {
logDebug("Querying SKU details.");
ArrayList skuList = new ArrayList();
skuList.addAll(inv.getAllOwnedSkus(itemType));
if (moreSkus != null) {
for (String sku : moreSkus) {
if (!skuList.contains(sku)) {
skuList.add(sku);
}
}
}
if (skuList.size() == 0) {
logDebug("queryPrices: nothing to do because there are no SKUs.");
return BILLING_RESPONSE_RESULT_OK;
}
// Split the sku list in blocks of no more than 20 elements.
ArrayList> packs = new ArrayList>();
ArrayList tempList;
int n = skuList.size() / 20;
int mod = skuList.size() % 20;
for (int i = 0; i < n; i++) {
tempList = new ArrayList();
for (String s : skuList.subList(i * 20, i * 20 + 20)) {
tempList.add(s);
}
packs.add(tempList);
}
if (mod != 0) {
tempList = new ArrayList();
for (String s : skuList.subList(n * 20, n * 20 + mod)) {
tempList.add(s);
}
packs.add(tempList);
}
for (ArrayList skuPartList : packs) {
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuPartList);
Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
itemType, querySkus);
if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
int response = getResponseCodeFromBundle(skuDetails);
if (response != BILLING_RESPONSE_RESULT_OK) {
logDebug("getSkuDetails() failed: " + getResponseDesc(response));
return response;
} else {
logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
return IABHELPER_BAD_RESPONSE;
}
}
ArrayList responseList = skuDetails.getStringArrayList(
RESPONSE_GET_SKU_DETAILS_LIST);
for (String thisResponse : responseList) {
SkuDetails d = new SkuDetails(itemType, thisResponse);
logDebug("Got sku details: " + d);
inv.addSkuDetails(d);
}
}
return BILLING_RESPONSE_RESULT_OK;
}
void consumeAsyncInternal(final List purchases,
final OnConsumeFinishedListener singleListener,
final OnConsumeMultiFinishedListener multiListener)
throws IabAsyncInProgressException {
final Handler handler = new Handler();
flagStartAsync("consume");
(new Thread(new Runnable() {
public void run() {
final List results = new ArrayList();
for (Purchase purchase : purchases) {
try {
consume(purchase);
results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
}
catch (IabException ex) {
results.add(ex.getResult());
}
}
flagEndAsync();
if (!mDisposed && singleListener != null) {
handler.post(new Runnable() {
public void run() {
singleListener.onConsumeFinished(purchases.get(0), results.get(0));
}
});
}
if (!mDisposed && multiListener != null) {
handler.post(new Runnable() {
public void run() {
multiListener.onConsumeMultiFinished(purchases, results);
}
});
}
}
})).start();
}
void logDebug(String msg) {
if (mDebugLog) Log.d(mDebugTag, msg);
}
void logError(String msg) {
Log.e(mDebugTag, "In-app billing error: " + msg);
}
void logWarn(String msg) {
Log.w(mDebugTag, "In-app billing warning: " + msg);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/util/IabResult.java
================================================
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alexstyl.specialdates.donate.util;
/**
* Represents the result of an in-app billing operation.
* A result is composed of a response code (an integer) and possibly a
* message (String). You can get those by calling
* {@link #getResponse} and {@link #getMessage()}, respectively. You
* can also inquire whether a result is a success or a failure by
* calling {@link #isSuccess()} and {@link #isFailure()}.
*/
public class IabResult {
int mResponse;
String mMessage;
public IabResult(int response, String message) {
mResponse = response;
if (message == null || message.trim().length() == 0) {
mMessage = IabHelper.getResponseDesc(response);
}
else {
mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
}
}
public int getResponse() { return mResponse; }
public String getMessage() { return mMessage; }
public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
public boolean isFailure() { return !isSuccess(); }
public String toString() { return "IabResult: " + getMessage(); }
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/util/Inventory.java
================================================
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alexstyl.specialdates.donate.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a block of information about in-app items.
* An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
*/
public class Inventory {
Map mSkuMap = new HashMap();
Map mPurchaseMap = new HashMap();
Inventory() { }
/** Returns the listing details for an in-app product. */
public SkuDetails getSkuDetails(String sku) {
return mSkuMap.get(sku);
}
/** Returns purchase information for a given product, or null if there is no purchase. */
public Purchase getPurchase(String sku) {
return mPurchaseMap.get(sku);
}
/** Returns whether or not there exists a purchase of the given product. */
public boolean hasPurchase(String sku) {
return mPurchaseMap.containsKey(sku);
}
/** Return whether or not details about the given product are available. */
public boolean hasDetails(String sku) {
return mSkuMap.containsKey(sku);
}
/**
* Erase a purchase (locally) from the inventory, given its product ID. This just
* modifies the Inventory object locally and has no effect on the server! This is
* useful when you have an existing Inventory object which you know to be up to date,
* and you have just consumed an item successfully, which means that erasing its
* purchase data from the Inventory you already have is quicker than querying for
* a new Inventory.
*/
public void erasePurchase(String sku) {
if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
}
/** Returns a list of all owned product IDs. */
List getAllOwnedSkus() {
return new ArrayList(mPurchaseMap.keySet());
}
/** Returns a list of all owned product IDs of a given type */
List getAllOwnedSkus(String itemType) {
List result = new ArrayList();
for (Purchase p : mPurchaseMap.values()) {
if (p.getItemType().equals(itemType)) result.add(p.getSku());
}
return result;
}
/** Returns a list of all purchases. */
List getAllPurchases() {
return new ArrayList(mPurchaseMap.values());
}
void addSkuDetails(SkuDetails d) {
mSkuMap.put(d.getSku(), d);
}
void addPurchase(Purchase p) {
mPurchaseMap.put(p.getSku(), p);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/util/Purchase.java
================================================
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alexstyl.specialdates.donate.util;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Represents an in-app billing purchase.
*/
public class Purchase {
String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
String mOrderId;
String mPackageName;
String mSku;
long mPurchaseTime;
int mPurchaseState;
String mDeveloperPayload;
String mToken;
String mOriginalJson;
String mSignature;
boolean mIsAutoRenewing;
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
mItemType = itemType;
mOriginalJson = jsonPurchaseInfo;
JSONObject o = new JSONObject(mOriginalJson);
mOrderId = o.optString("orderId");
mPackageName = o.optString("packageName");
mSku = o.optString("productId");
mPurchaseTime = o.optLong("purchaseTime");
mPurchaseState = o.optInt("purchaseState");
mDeveloperPayload = o.optString("developerPayload");
mToken = o.optString("token", o.optString("purchaseToken"));
mIsAutoRenewing = o.optBoolean("autoRenewing");
mSignature = signature;
}
public String getItemType() { return mItemType; }
public String getOrderId() { return mOrderId; }
public String getPackageName() { return mPackageName; }
public String getSku() { return mSku; }
public long getPurchaseTime() { return mPurchaseTime; }
public int getPurchaseState() { return mPurchaseState; }
public String getDeveloperPayload() { return mDeveloperPayload; }
public String getToken() { return mToken; }
public String getOriginalJson() { return mOriginalJson; }
public String getSignature() { return mSignature; }
public boolean isAutoRenewing() { return mIsAutoRenewing; }
@Override
public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/util/Security.java
================================================
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alexstyl.specialdates.donate.util;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
/**
* Security-related methods. For a secure implementation, all of this code
* should be implemented on a server that communicates with the
* application on the device. For the sake of simplicity and clarity of this
* example, this code is included here and is executed on the device. If you
* must verify the purchases on the phone, you should obfuscate this code to
* make it harder for an attacker to replace the code with stubs that treat all
* purchases as verified.
*/
@SuppressWarnings("all")
public class Security {
private static final String TAG = "IABUtil/Security";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Verifies that the data was signed with the given signature, and returns
* the verified purchase. The data is in JSON format and signed
* with a private key. The data also contains the {@link PurchaseState}
* and product ID of the purchase.
* @param base64PublicKey the base64-encoded public key to use for verifying.
* @param signedData the signed JSON string (signed, not encrypted)
* @param signature the signature for the data, signed with the private key
*/
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
TextUtils.isEmpty(signature)) {
Log.e(TAG, "Purchase verification failed: missing data.");
return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature);
}
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* @param encodedPublicKey Base64-encoded public key
* @throws IllegalArgumentException if encodedPublicKey is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* @param publicKey public key associated with the developer account
* @param signedData signed data from server
* @param signature server signature
* @return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
byte[] signatureBytes;
try {
signatureBytes = Base64.decode(signature, Base64.DEFAULT);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Base64 decoding failed.");
return false;
}
try {
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(signatureBytes)) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
}
return false;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/donate/util/SkuDetails.java
================================================
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alexstyl.specialdates.donate.util;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Represents an in-app product's listing details.
*/
public class SkuDetails {
private final String mItemType;
private final String mSku;
private final String mType;
private final String mPrice;
private final long mPriceAmountMicros;
private final String mPriceCurrencyCode;
private final String mTitle;
private final String mDescription;
private final String mJson;
public SkuDetails(String jsonSkuDetails) throws JSONException {
this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
}
public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
mItemType = itemType;
mJson = jsonSkuDetails;
JSONObject o = new JSONObject(mJson);
mSku = o.optString("productId");
mType = o.optString("type");
mPrice = o.optString("price");
mPriceAmountMicros = o.optLong("price_amount_micros");
mPriceCurrencyCode = o.optString("price_currency_code");
mTitle = o.optString("title");
mDescription = o.optString("description");
}
public String getSku() { return mSku; }
public String getType() { return mType; }
public String getPrice() { return mPrice; }
public long getPriceAmountMicros() { return mPriceAmountMicros; }
public String getPriceCurrencyCode() { return mPriceCurrencyCode; }
public String getTitle() { return mTitle; }
public String getDescription() { return mDescription; }
@Override
public String toString() {
return "SkuDetails:" + mJson;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/ContactsObserver.java
================================================
package com.alexstyl.specialdates.events;
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.provider.ContactsContract;
import com.alexstyl.specialdates.EventsUpdateTrigger;
public final class ContactsObserver extends ContentObserver implements EventsUpdateTrigger {
private static final Uri URI = ContactsContract.Contacts.CONTENT_URI;
private final ContentResolver resolver;
private Callback callback;
public ContactsObserver(ContentResolver resolver) {
super(new Handler());
this.resolver = resolver;
}
@Override
public void startObserving(Callback callback) {
this.callback = callback;
resolver.registerContentObserver(URI, false, this);
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
callback.onMonitorTriggered();
}
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
callback.onMonitorTriggered();
}
@Override
public void stopObserving() {
resolver.unregisterContentObserver(this);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/PreferenceChangedEventsUpdateTrigger.java
================================================
package com.alexstyl.specialdates.events;
import android.content.SharedPreferences;
import android.content.res.Resources;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.EventsUpdateTrigger;
import java.util.ArrayList;
import java.util.List;
public final class PreferenceChangedEventsUpdateTrigger implements EventsUpdateTrigger {
private final EasyPreferences preferences;
private final List keys;
private SharedPreferences.OnSharedPreferenceChangeListener listener;
public PreferenceChangedEventsUpdateTrigger(EasyPreferences preferences, Resources strings, int firstKeys, int... keys) {
this.preferences = preferences;
this.keys = new ArrayList<>(keys.length + 1);
this.keys.add(strings.getString(firstKeys));
for (int key : keys) {
this.keys.add(strings.getString(key));
}
}
@Override
public void startObserving(final Callback callback) {
listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (isAKeyIcareAbout(key)) {
callback.onMonitorTriggered();
}
}
};
preferences.addOnPreferenceChangedListener(listener);
}
private boolean isAKeyIcareAbout(String key) {
return keys.contains(key);
}
@Override
public void stopObserving() {
preferences.removeOnPreferenceChagnedListener(listener);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/bankholidays/BankHolidaysModule.java
================================================
package com.alexstyl.specialdates.events.bankholidays;
import android.content.Context;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.events.namedays.calendar.OrthodoxEasterCalculator;
import dagger.Module;
import dagger.Provides;
@Module
public class BankHolidaysModule {
@Provides
BankHolidaysUserSettings userSettings(Context context) {
EasyPreferences preferences = EasyPreferences.createForDefaultPreferences(context);
return new BankHolidaysPreferences(preferences);
}
@Provides
GreekBankHolidaysCalculator greekBankHolidaysCalculator(OrthodoxEasterCalculator calculator) {
return new GreekBankHolidaysCalculator(calculator);
}
@Provides
BankHolidayProvider provider(GreekBankHolidaysCalculator bankHolidaysCalculator) {
return new BankHolidayProvider(bankHolidaysCalculator);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/bankholidays/BankHolidaysPreferences.java
================================================
package com.alexstyl.specialdates.events.bankholidays;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.R;
public class BankHolidaysPreferences implements BankHolidaysUserSettings {
private final EasyPreferences preferences;
BankHolidaysPreferences(EasyPreferences preferences) {
this.preferences = preferences;
}
@Override
public boolean isEnabled() {
return preferences.getBoolean(R.string.key_enable_bank_holidays, R.bool.isBankholidaysSupported);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/database/ContactColumns.java
================================================
package com.alexstyl.specialdates.events.database;
@SuppressWarnings("all")
interface ContactColumns {
String CONTACT_ID = "contact_id";
String DISPLAY_NAME = "display_name";
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/database/DatabaseContract.java
================================================
package com.alexstyl.specialdates.events.database;
import android.provider.BaseColumns;
public final class DatabaseContract {
public static final class AnnualEventsContract implements BaseColumns, ContactColumns, EventColumns {
public static final String TABLE_NAME = "annual_events";
}
private DatabaseContract() {
// hide this
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/database/EventSQLiteOpenHelper.kt
================================================
package com.alexstyl.specialdates.events.database
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns._ID
import com.alexstyl.specialdates.events.database.ContactColumns.CONTACT_ID
import com.alexstyl.specialdates.events.database.ContactColumns.DISPLAY_NAME
import com.alexstyl.specialdates.events.database.DatabaseContract.AnnualEventsContract.TABLE_NAME
import com.alexstyl.specialdates.events.database.EventColumns.DATE
import com.alexstyl.specialdates.events.database.EventColumns.DEVICE_EVENT_ID
import com.alexstyl.specialdates.events.database.EventColumns.EVENT_TYPE
import com.alexstyl.specialdates.events.database.EventColumns.SOURCE
import com.alexstyl.specialdates.events.database.EventColumns.VISIBLE
class EventSQLiteOpenHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS dynamic_events;")
db.execSQL("DROP TABLE IF EXISTS annual_events;")
onCreate(db)
}
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(("CREATE TABLE $TABLE_NAME ("
+ "$_ID INTEGER NOT NULL, "
+ "$DISPLAY_NAME TEXT NOT NULL, "
+ "$DEVICE_EVENT_ID INTEGER NOT NULL, "
+ "$CONTACT_ID INTEGER NOT NULL, "
+ "$DATE TEXT NOT NULL, "
+ "$EVENT_TYPE INTEGER NOT NULL, "
+ "$SOURCE INTEGER NOT NULL, "
+ "$VISIBLE INTEGER NOT NULL, "
+ "PRIMARY KEY ($_ID)"
+ ");"))
}
companion object {
const val DATABASE_VERSION = 5
private const val DATABASE_NAME = "events.db"
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/NamedayModule.java
================================================
package com.alexstyl.specialdates.events.namedays;
import android.content.Context;
import android.content.res.Resources;
import com.alexstyl.specialdates.date.Date;
import com.alexstyl.specialdates.events.namedays.calendar.NamedayCalendar;
import com.alexstyl.specialdates.events.namedays.calendar.OrthodoxEasterCalculator;
import com.alexstyl.specialdates.events.namedays.calendar.resource.AndroidJSONResourceLoader;
import com.alexstyl.specialdates.events.namedays.calendar.resource.NamedayCalendarProvider;
import com.alexstyl.specialdates.events.namedays.calendar.resource.NamedayJSONProvider;
import com.alexstyl.specialdates.events.namedays.calendar.resource.RomanianEasterSpecialCalculator;
import com.alexstyl.specialdates.events.namedays.calendar.resource.SpecialNamedaysHandlerFactory;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
@Singleton
public class NamedayModule {
@Provides
NamedayJSONProvider namedayJSONProvider(Resources resources) {
return new NamedayJSONProvider(new AndroidJSONResourceLoader(resources));
}
@Provides
RomanianEasterSpecialCalculator romanianEasterSpecialCalculator(OrthodoxEasterCalculator calculator) {
return new RomanianEasterSpecialCalculator(calculator);
}
@Provides
OrthodoxEasterCalculator calculator() {
return new OrthodoxEasterCalculator();
}
@Provides
SpecialNamedaysHandlerFactory handlerFactory(OrthodoxEasterCalculator easterCalculator,
RomanianEasterSpecialCalculator romanianEasterCalculator) {
return new SpecialNamedaysHandlerFactory(easterCalculator, romanianEasterCalculator);
}
@Provides
@Singleton
NamedayCalendarProvider provider(OrthodoxEasterCalculator easterCalculator, NamedayJSONProvider namedayJSONProvider, SpecialNamedaysHandlerFactory factory) {
return new NamedayCalendarProvider(
namedayJSONProvider,
factory
);
}
@Provides
NamedayUserSettings userSettings(Context context) {
return new NamedayPreferences(context);
}
@Provides
NamedayCalendar calendar(NamedayUserSettings settings, NamedayCalendarProvider namedayCalendarProvider) {
NamedayLocale selectedLanguage = settings.getSelectedLanguage();
int year = Date.Companion.getCURRENT_YEAR();
return namedayCalendarProvider.loadNamedayCalendarForLocale(selectedLanguage, year);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/NamedayPreferences.java
================================================
package com.alexstyl.specialdates.events.namedays;
import android.content.Context;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.R;
public final class NamedayPreferences implements NamedayUserSettings {
private static final String DEFAULT_LOCALE = NamedayLocale.GREEK.getCountryCode();
private final boolean enabledByDefault;
private final EasyPreferences preferences;
NamedayPreferences(Context context) {
this.preferences = EasyPreferences.createForDefaultPreferences(context);
this.enabledByDefault = shouldNamedaysBeEnabledByDefault(context);
}
@Override
public void setSelectedLanguage(String language) {
preferences.setString(R.string.key_nameday_lang, language);
}
@Override
public NamedayLocale getSelectedLanguage() {
String lang = preferences.getString(R.string.key_nameday_lang, DEFAULT_LOCALE);
return NamedayLocale.from(lang);
}
@Override
public boolean isEnabled() {
return preferences.getBoolean(R.string.key_enable_namedays, enabledByDefault);
}
@Override
public boolean isEnabledForContactsOnly() {
return preferences.getBoolean(R.string.key_namedays_contacts_only, false);
}
@Override
public void setEnabledForContactsOnly(boolean onlyForContacts) {
preferences.setBoolean(R.string.key_namedays_contacts_only, onlyForContacts);
}
@Override
public boolean shouldLookupAllNames() {
return preferences.getBoolean(R.string.key_namedays_full_name, false);
}
private static boolean shouldNamedaysBeEnabledByDefault(Context context) {
return context.getResources().getBoolean(R.bool.isNamedaySupported);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/AndroidNamedaysOnADayView.kt
================================================
package com.alexstyl.specialdates.events.namedays.activity
class AndroidNamedaysOnADayView(private val screenAdapter: NamedaysScreenAdapter) : NamedaysOnADayView {
override fun displayNamedays(viewModels: List) {
screenAdapter.display(viewModels)
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/CelebratingContactViewHolder.kt
================================================
package com.alexstyl.specialdates.events.namedays.activity
import android.view.View
import android.widget.TextView
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.images.ImageLoader
import com.alexstyl.specialdates.ui.widget.ColorImageView
class CelebratingContactViewHolder(view: View,
private val imageLoader: ImageLoader,
private val avatarView: ColorImageView,
private val contactNameView: TextView)
: NamedayScreenViewHolder(view) {
override fun bind(viewModel: CelebratingContactViewModel, onContactClicked: (Contact) -> Unit) {
val contact = viewModel.contact
contactNameView.text = contact.displayName.toString()
avatarView.setLetter(viewModel.letter)
avatarView.setCircleColorVariant(viewModel.colorVariant)
imageLoader.load(contact.imagePath)
.asCircle()
.into(avatarView.imageView)
itemView.setOnClickListener { onContactClicked(contact) }
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/NameViewHolder.kt
================================================
package com.alexstyl.specialdates.events.namedays.activity
import android.view.View
import android.widget.TextView
import com.alexstyl.specialdates.contact.Contact
class NameViewHolder(view: View,
private val nameView: TextView)
: NamedayScreenViewHolder(view) {
override fun bind(viewModel: NamedaysViewModel, onContactClicked: (Contact) -> Unit) {
nameView.text = viewModel.name
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/NamedayScreenViewHolder.kt
================================================
package com.alexstyl.specialdates.events.namedays.activity
import android.support.v7.widget.RecyclerView
import android.view.View
import com.alexstyl.specialdates.contact.Contact
abstract class NamedayScreenViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(viewModel: T, onContactClicked: (Contact) -> Unit)
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/NamedaysInADayModule.java
================================================
package com.alexstyl.specialdates.events.namedays.activity;
import com.alexstyl.specialdates.contact.ContactsProvider;
import com.alexstyl.specialdates.events.namedays.NamedayUserSettings;
import com.alexstyl.specialdates.events.namedays.calendar.NamedayCalendar;
import com.alexstyl.specialdates.ui.widget.AndroidLetterPainter;
import dagger.Module;
import dagger.Provides;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@Module
public class NamedaysInADayModule {
@Provides
NamedaysViewModelFactory viewModelFactory(AndroidLetterPainter letterPainter) {
return new NamedaysViewModelFactory(letterPainter);
}
@Provides
NamedaysInADayPresenter presenter(NamedayCalendar calendar,
NamedaysViewModelFactory namedaysViewModelFactory,
ContactsProvider contactsProvider,
NamedayUserSettings namedayUserSettings) {
return new NamedaysInADayPresenter(calendar, namedaysViewModelFactory, contactsProvider, namedayUserSettings, Schedulers.io(), AndroidSchedulers.mainThread());
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/NamedaysOnADayActivity.kt
================================================
package com.alexstyl.specialdates.events.namedays.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.widget.TextView
import com.alexstyl.specialdates.MementoApplication
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.analytics.Analytics
import com.alexstyl.specialdates.analytics.Screen
import com.alexstyl.specialdates.date.Date
import com.alexstyl.specialdates.date.DateLabelCreator
import com.alexstyl.specialdates.date.getDateExtraOrThrow
import com.alexstyl.specialdates.date.putExtraDate
import com.alexstyl.specialdates.images.ImageLoader
import com.alexstyl.specialdates.ui.base.ThemedMementoActivity
import com.alexstyl.specialdates.ui.widget.MementoToolbar
import javax.inject.Inject
class NamedaysOnADayActivity : ThemedMementoActivity() {
@Inject
lateinit var imageLoader: ImageLoader
@Inject
lateinit var presenter: NamedaysInADayPresenter
@Inject
lateinit var dateLabelCreator: DateLabelCreator
@Inject
lateinit var analytics: Analytics
private var dateView: TextView? = null
private var namedaysOnADayNavigator: NamedaysOnADayNavigator? = null
private lateinit var view: NamedaysOnADayView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_namedays)
val applicationModule = (application as MementoApplication).applicationModule
applicationModule.inject(this)
analytics.trackScreen(Screen.NAMEDAYS)
namedaysOnADayNavigator = NamedaysOnADayNavigator(this, analytics)
val toolbar = findViewById(R.id.memento_toolbar)
toolbar.displayNavigationIconAsUp()
setSupportActionBar(toolbar)
dateView = findViewById(R.id.namedays_date)
val recyclerView = findViewById(R.id.namedays_list)
recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
val date = intent.getDateExtraOrThrow()
dateView?.text = dateLabelCreator.createWithYearPreferred(date)
val layoutInflater = LayoutInflater.from(this)
val adapter = NamedaysScreenAdapter(
NamedaysScreenViewHolderFactory(layoutInflater, imageLoader),
{ contact -> namedaysOnADayNavigator?.toContactDetails(contact) }
)
recyclerView.adapter = adapter
view = AndroidNamedaysOnADayView(adapter)
}
override fun onStart() {
super.onStart()
val date = intent.getDateExtraOrThrow()
presenter.startPresenting(view, date)
}
override fun onStop() {
super.onStop()
presenter.stopPresenting()
}
companion object {
fun getStartIntent(context: Context, date: Date): Intent {
return Intent(context, NamedaysOnADayActivity::class.java)
.putExtraDate(date)
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/NamedaysOnADayNavigator.kt
================================================
package com.alexstyl.specialdates.events.namedays.activity
import android.app.Activity
import com.alexstyl.specialdates.analytics.Analytics
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.person.PersonActivity
class NamedaysOnADayNavigator(private val activity: Activity, private val analytics: Analytics) {
fun toContactDetails(contact: Contact) {
val intent = PersonActivity.buildIntentFor(activity, contact)
activity.startActivity(intent)
analytics.trackNamedaysScreen()
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/NamedaysScreenAdapter.java
================================================
package com.alexstyl.specialdates.events.namedays.activity;
import android.support.annotation.NonNull;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import com.alexstyl.specialdates.contact.Contact;
import java.util.ArrayList;
import java.util.List;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
public class NamedaysScreenAdapter extends RecyclerView.Adapter {
private final List viewModels = new ArrayList<>();
private final NamedaysScreenViewHolderFactory viewholderFactory;
private final Function1 onContactClicked;
public NamedaysScreenAdapter(NamedaysScreenViewHolderFactory viewholderFactory, Function1 onContactClicked) {
this.viewholderFactory = viewholderFactory;
this.onContactClicked = onContactClicked;
}
@Override
public int getItemViewType(int position) {
return viewModels.get(position).getViewType();
}
@NonNull
@Override
public NamedayScreenViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return viewholderFactory.viewHolderFor(parent, viewType);
}
@Override
@SuppressWarnings("unchecked")
public void onBindViewHolder(@NonNull NamedayScreenViewHolder holder, int position) {
holder.bind(viewModels.get(position), onContactClicked);
}
void display(List viewModels) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new NamedaysViewModelDiff(this.viewModels, viewModels));
this.viewModels.clear();
this.viewModels.addAll(viewModels);
diffResult.dispatchUpdatesTo(this);
}
@Override
public int getItemCount() {
return viewModels.size();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/NamedaysScreenViewHolderFactory.kt
================================================
package com.alexstyl.specialdates.events.namedays.activity
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.images.ImageLoader
import com.alexstyl.specialdates.ui.widget.ColorImageView
class NamedaysScreenViewHolderFactory(private val layoutInflater: LayoutInflater,
private val imageLoader: ImageLoader) {
fun viewHolderFor(parent: ViewGroup, @NamedayScreenViewType viewType: Int): NamedayScreenViewHolder<*> {
return when (viewType) {
NamedayScreenViewType.NAMEDAY -> {
val view = layoutInflater.inflate(R.layout.row_nameday_name, parent, false)
val nameView = view.findViewById(R.id.nameday_name)
NameViewHolder(view, nameView)
}
NamedayScreenViewType.CONTACT -> {
val view = layoutInflater.inflate(R.layout.row_nameday_contact, parent, false)
val nameView = view.findViewById(R.id.row_nameday_contact_name)
val avatarView = view.findViewById(R.id.row_nameday_contact_avatar)
CelebratingContactViewHolder(view, imageLoader, avatarView, nameView)
}
else -> throw UnsupportedOperationException("Unsupported view type $viewType")
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/activity/NamedaysViewModelDiff.java
================================================
package com.alexstyl.specialdates.events.namedays.activity;
import android.support.v7.util.DiffUtil;
import java.util.List;
final class NamedaysViewModelDiff extends DiffUtil.Callback {
private final List oldModels;
private final List newModels;
NamedaysViewModelDiff(List oldModels, List newModels) {
this.oldModels = oldModels;
this.newModels = newModels;
}
@Override
public int getOldListSize() {
return oldModels.size();
}
@Override
public int getNewListSize() {
return newModels.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldModels.get(oldItemPosition).getViewType() == newModels.get(newItemPosition).getViewType()
&& oldModels.get(oldItemPosition).getId() == newModels.get(newItemPosition).getId();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldModels.get(oldItemPosition).equals(newModels.get(newItemPosition));
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/namedays/calendar/resource/AndroidJSONResourceLoader.kt
================================================
package com.alexstyl.specialdates.events.namedays.calendar.resource
import android.content.res.Resources
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.events.namedays.NamedayLocale
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import org.json.JSONException
import org.json.JSONObject
class AndroidJSONResourceLoader(private val resources: Resources) : NamedayJSONResourceLoader {
@Throws(JSONException::class)
override fun loadJSON(locale: NamedayLocale): JSONObject {
val inputStream = resources.openRawResource(locale.rawResId())
val outputStream = ByteArrayOutputStream()
var ctr: Int
try {
ctr = inputStream.read()
while (ctr != -1) {
outputStream.write(ctr)
ctr = inputStream.read()
}
inputStream.close()
return JSONObject(outputStream.toString("UTF-8"))
} catch (e: IOException) {
throw JSONException(e.message)
} catch (e: JSONException) {
throw JSONException(e.message)
}
}
}
private fun NamedayLocale.rawResId(): Int = when(this){
NamedayLocale.GREEK -> R.raw.gr_namedays
NamedayLocale.ROMANIAN -> R.raw.ro_namedays
NamedayLocale.RUSSIAN -> R.raw.ru_namedays
NamedayLocale.LATVIAN -> R.raw.lv_namedays
NamedayLocale.LATVIAN_EXTENDED -> R.raw.lv_ext_namedays
NamedayLocale.SLOVAK -> R.raw.sk_namedays
NamedayLocale.ITALIAN -> R.raw.it_namedays
NamedayLocale.CZECH -> R.raw.cs_namedays
NamedayLocale.HUNGARIAN -> R.raw.hu_namedays
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/peopleevents/AndroidPeopleEventsPersister.kt
================================================
package com.alexstyl.specialdates.events.peopleevents
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.contact.ContactSource
import com.alexstyl.specialdates.date.ContactEvent
import com.alexstyl.specialdates.events.database.DatabaseContract.AnnualEventsContract
import com.alexstyl.specialdates.events.database.EventColumns
import com.alexstyl.specialdates.events.database.EventTypeId.TYPE_NAMEDAY
class AndroidPeopleEventsPersister(private val helper: SQLiteOpenHelper,
private val marshaller: ContactEventsMarshaller,
private val tracker: CrashAndErrorTracker)
: PeopleEventsPersister {
override fun deleteAllNamedays() {
helper.writableDatabase
.executeTransaction {
delete(
AnnualEventsContract.TABLE_NAME,
"${AnnualEventsContract.EVENT_TYPE} == $TYPE_NAMEDAY",
null)
}
}
override fun deleteAllEventsOfSource(@ContactSource source: Int) {
helper.writableDatabase
.executeTransaction {
delete(AnnualEventsContract.TABLE_NAME,
AnnualEventsContract.SOURCE + "==" + source,
null)
}
}
override fun deleteAllDeviceEvents() {
helper.writableDatabase
.executeTransaction {
delete(AnnualEventsContract.TABLE_NAME,
"${EventColumns.SOURCE} == ${ContactSource.SOURCE_DEVICE}" +
" AND ${EventColumns.EVENT_TYPE} != ${StandardEventType.NAMEDAY.id}"
, null)
}
}
override fun insertAnnualEvents(events: List) {
helper.writableDatabase
.executeTransaction {
marshaller
.marshall(events)
.forEach { contentValues ->
insert(AnnualEventsContract.TABLE_NAME, null, contentValues)
}
}
}
override fun markContactAsVisible(contact: Contact) {
helper.writableDatabase
.executeTransaction {
update(
AnnualEventsContract.TABLE_NAME,
visible(),
AnnualEventsContract.CONTACT_ID + " = " + contact.contactID
+ " AND " + AnnualEventsContract.SOURCE + " = " + contact.source, null)
}
}
private fun visible(): ContentValues {
return ContentValues(1).apply {
put(AnnualEventsContract.VISIBLE, 1)
}
}
override fun markContactAsHidden(contact: Contact) {
helper.writableDatabase
.executeTransaction {
update(AnnualEventsContract.TABLE_NAME,
hidden(),
AnnualEventsContract.CONTACT_ID + " = " + contact.contactID
+ " AND " + AnnualEventsContract.SOURCE + " = " + contact.source, null)
}
}
private fun hidden(): ContentValues {
val values = ContentValues(1)
values.put(AnnualEventsContract.VISIBLE, 0)
return values
}
override fun getVisibilityFor(contact: Contact): Boolean {
val database = helper.writableDatabase
// TODO just COUNT() events the contact has
val query = database.query(
AnnualEventsContract.TABLE_NAME, null,
AnnualEventsContract.CONTACT_ID + " == " + contact.contactID
+ " AND " + AnnualEventsContract.SOURCE + " == " + contact.source
+ " AND " + AnnualEventsContract.VISIBLE + " = 1", null, null, null, null
)
val count = query.count
query.close()
return count > 0
}
private inline fun SQLiteDatabase.executeTransaction(function: SQLiteDatabase.() -> Unit) {
try {
this.beginTransaction()
function(this)
this.setTransactionSuccessful()
} catch (e: SQLiteException) {
tracker.track(e)
} finally {
this.endTransaction()
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/peopleevents/AndroidPeopleEventsProvider.kt
================================================
package com.alexstyl.specialdates.events.peopleevents
import android.database.Cursor
import android.database.MergeCursor
import android.database.sqlite.SQLiteDatabase
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.Optional
import com.alexstyl.specialdates.SQLArgumentBuilder
import com.alexstyl.specialdates.contact.Contact
import com.alexstyl.specialdates.contact.ContactNotFoundException
import com.alexstyl.specialdates.contact.ContactSource
import com.alexstyl.specialdates.contact.Contacts
import com.alexstyl.specialdates.contact.ContactsProvider
import com.alexstyl.specialdates.date.ContactEvent
import com.alexstyl.specialdates.date.Date
import com.alexstyl.specialdates.date.DateParser
import com.alexstyl.specialdates.date.TimePeriod
import com.alexstyl.specialdates.events.database.DatabaseContract.AnnualEventsContract
import com.alexstyl.specialdates.events.database.EventSQLiteOpenHelper
import com.alexstyl.specialdates.events.database.EventTypeId
import com.alexstyl.specialdates.events.database.EventTypeId.TYPE_CUSTOM
import com.novoda.notils.logger.simple.Log
class AndroidPeopleEventsProvider(private val eventSQLHelper: EventSQLiteOpenHelper,
private val contactsProvider: ContactsProvider,
private val customEventProvider: CustomEventProvider,
private val dateParser: DateParser,
private val tracker: CrashAndErrorTracker,
private val shortDateLabelCreator: ShortDateLabelCreator) : PeopleEventsProvider {
override fun fetchEventsOn(date: Date): ContactEventsOnADate {
return ContactEventsOnADate.createFrom(date, fetchEventsBetween(TimePeriod.between(date, date)))
}
override fun fetchEventsBetween(timePeriod: TimePeriod): List {
val cursor = queryEventsFor(timePeriod)
val contactEvents = ArrayList(cursor.count)
val deviceIds = ArrayList(cursor.count)
val facebookIds = ArrayList(cursor.count)
while (cursor.moveToNext()) {
val contactId = getContactIdFrom(cursor)
val source = getContactSourceFrom(cursor)
when (source) {
ContactSource.SOURCE_DEVICE -> deviceIds.add(contactId)
ContactSource.SOURCE_FACEBOOK -> facebookIds.add(contactId)
else -> throw UnsupportedOperationException("Source $source not managed")
}
}
val deviceContacts = contactsProvider.getContacts(deviceIds, ContactSource.SOURCE_DEVICE)
val facebookContacts = contactsProvider.getContacts(facebookIds, ContactSource.SOURCE_FACEBOOK)
val contacts = HashMap()
contacts[ContactSource.SOURCE_DEVICE] = deviceContacts
contacts[ContactSource.SOURCE_FACEBOOK] = facebookContacts
cursor.moveToFirst()
while (!cursor.isAfterLast) {
try {
val contactsOfSource = contacts[getContactSourceFrom(cursor)]
val contact = contactsOfSource?.getContact(getContactIdFrom(cursor))
if (contact != null) {
val contactEvent = getContactEventFrom(cursor, contact)
contactEvents.add(contactEvent)
}
} catch (e: ContactNotFoundException) {
tracker.track(e)
}
cursor.moveToNext()
}
cursor.close()
return contactEvents.toList()
}
override fun fetchEventsFor(contact: Contact): List {
val contactEvents = ArrayList()
val cursor = queryEventsOf(contact)
while (cursor.moveToNext()) {
try {
val contactEvent = getContactEventFrom(cursor, contact)
contactEvents.add(contactEvent)
} catch (e: ContactNotFoundException) {
Log.w(e)
}
}
cursor.close()
return contactEvents.toList()
}
private fun queryEventsFor(timeDuration: TimePeriod): Cursor {
return if (isWithinTheSameYear(timeDuration)) {
queryPeopleEvents(timeDuration, AnnualEventsContract.DATE + " ASC")
} else {
queryAllYearsIn(timeDuration)
}
}
private fun queryEventsOf(contact: Contact): Cursor {
val selectArgs = arrayOf(contact.contactID.toString(), contact.source.toString())
// query database
return eventSQLHelper.readableDatabase.query(
AnnualEventsContract.TABLE_NAME,
PROJECTION,
AnnualEventsContract.CONTACT_ID + " = ? "
+ "AND " + AnnualEventsContract.SOURCE + " = ?",
selectArgs, null, null, null
)
}
private fun queryPeopleEvents(timePeriod: TimePeriod, sortOrder: String): Cursor {
val selectArgs = arrayOf(SQLArgumentBuilder.dateWithoutYear(timePeriod.startingDate),
SQLArgumentBuilder.dateWithoutYear(timePeriod.endingDate)
)
return eventSQLHelper.readableDatabase.query(
AnnualEventsContract.TABLE_NAME,
PROJECTION,
DATE_BETWEEN_IGNORING_YEAR,
selectArgs, null, null,
sortOrder
)
}
private fun queryAllYearsIn(timeDuration: TimePeriod): Cursor {
val firstHalf = firstHalfOf(timeDuration)
val cursors = arrayOfNulls(2)
cursors[0] = queryPeopleEvents(firstHalf, AnnualEventsContract.DATE + " ASC")
val secondHalf = secondHalfOf(timeDuration)
cursors[1] = queryPeopleEvents(secondHalf, AnnualEventsContract.DATE + " ASC")
return MergeCursor(cursors)
}
private fun isWithinTheSameYear(timeDuration: TimePeriod): Boolean {
return timeDuration.startingDate.year == timeDuration.endingDate.year
}
override fun findClosestEventDateOnOrAfter(date: Date): Date? =
eventSQLHelper
.readableDatabase
.queryFirstEventOnOrAfter(date)
.use { cursor ->
return if (cursor.moveToFirst()) {
return cursor.getDate().withYear(date.year)
} else {
null
}
}
private fun Date.withYear(year: Int): Date = Date.on(this.dayOfMonth, this.month, year)
private fun SQLiteDatabase.queryFirstEventOnOrAfter(date: Date): Cursor =
query(
AnnualEventsContract.TABLE_NAME,
AndroidPeopleEventsProvider.PEOPLE_PROJECTION,
"${AndroidPeopleEventsProvider.DATE_COLUMN_WITHOUT_YEAR} >= ?",
monthAndDayOf(date),
null, null,
"${AndroidPeopleEventsProvider.DATE_COLUMN_WITHOUT_YEAR} ASC LIMIT 1")
private fun monthAndDayOf(date: Date): Array {
return arrayOf(shortDateLabelCreator.createLabelWithNoYearFor(date))
}
private fun getEventType(cursor: Cursor): EventType {
val eventTypeIndex = cursor.getColumnIndexOrThrow(AnnualEventsContract.EVENT_TYPE)
@EventTypeId val rawEventType = cursor.getInt(eventTypeIndex)
if (rawEventType == TYPE_CUSTOM) {
val deviceEventIdFrom = getDeviceEventIdFrom(cursor)
return if (deviceEventIdFrom.isPresent) {
queryCustomEvent(deviceEventIdFrom.get())
} else StandardEventType.OTHER
}
return StandardEventType.fromId(rawEventType)
}
@Throws(ContactNotFoundException::class)
private fun getContactEventFrom(cursor: Cursor, contact: Contact): ContactEvent {
val date = cursor.getDate()
val eventType = getEventType(cursor)
val eventId = getDeviceEventIdFrom(cursor)
return ContactEvent(eventId, eventType, date, contact)
}
@ContactSource
private fun getContactSourceFrom(cursor: Cursor): Int {
val sourceTypeIndex = cursor.getColumnIndexOrThrow(AnnualEventsContract.SOURCE)
return cursor.getInt(sourceTypeIndex)
}
private fun queryCustomEvent(deviceId: Long): EventType {
return customEventProvider.getEventWithId(deviceId)
}
private fun Cursor.getDate(): Date {
val index = getColumnIndexOrThrow(AnnualEventsContract.DATE)
val rawDate = getString(index)
return dateParser.parse(rawDate)
}
companion object {
private const val DATE_FROM = "substr(" + AnnualEventsContract.DATE + ",-5) >= ?"
private const val DATE_TO = "substr(" + AnnualEventsContract.DATE + ",-5) <= ?"
private const val DATE_BETWEEN_IGNORING_YEAR = DATE_FROM + " AND " + DATE_TO + " AND " + AnnualEventsContract.VISIBLE + " == 1"
private val PEOPLE_PROJECTION = arrayOf(AnnualEventsContract.DATE)
private val PROJECTION = arrayOf(
AnnualEventsContract.CONTACT_ID,
AnnualEventsContract.DEVICE_EVENT_ID,
AnnualEventsContract.DATE,
AnnualEventsContract.EVENT_TYPE,
AnnualEventsContract.SOURCE
)
/*
We use this column in order to be able to do comparisons of dates, without having to worry about the year
So, instead of a full date 1990-12-19, this will return 12-19. Similarwise for --12-19.
an example in use: select * from annual_events WHERE substr(date,-5) >= '03-04' ORDER BY substr(date,-5) asc LIMIT 1
*/
private const val DATE_COLUMN_WITHOUT_YEAR = "substr(" + AnnualEventsContract.DATE + ", -5) "
private fun firstHalfOf(timeDuration: TimePeriod): TimePeriod {
return TimePeriod.between(
timeDuration.startingDate,
Date.endOfYear(timeDuration.startingDate.year)
)
}
private fun secondHalfOf(timeDuration: TimePeriod): TimePeriod {
return TimePeriod.between(
Date.startOfYear(timeDuration.endingDate.year),
timeDuration.endingDate
)
}
private fun getContactIdFrom(cursor: Cursor): Long {
val contactIdIndex = cursor.getColumnIndexOrThrow(AnnualEventsContract.CONTACT_ID)
return cursor.getLong(contactIdIndex)
}
private fun getDeviceEventIdFrom(cursor: Cursor): Optional {
val eventId = cursor.getColumnIndexOrThrow(AnnualEventsContract.DEVICE_EVENT_ID)
val deviceEventId = cursor.getLong(eventId)
return if (isALegitEventId(deviceEventId)) {
Optional.absent()
} else Optional(deviceEventId)
}
private fun isALegitEventId(deviceEventId: Long): Boolean {
return deviceEventId == -1L
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/peopleevents/AndroidPeopleEventsRepository.kt
================================================
package com.alexstyl.specialdates.events.peopleevents
import android.content.ContentResolver
import android.database.Cursor
import android.provider.ContactsContract
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.Optional
import com.alexstyl.specialdates.contact.ContactNotFoundException
import com.alexstyl.specialdates.contact.ContactSource.SOURCE_DEVICE
import com.alexstyl.specialdates.contact.ContactsProvider
import com.alexstyl.specialdates.date.ContactEvent
import com.alexstyl.specialdates.date.Date
import com.alexstyl.specialdates.date.DateParseException
import com.alexstyl.specialdates.date.DateParser
import com.novoda.notils.logger.simple.Log
class AndroidPeopleEventsRepository(private val contentResolver: ContentResolver,
private val contactsProvider: ContactsProvider,
private val dateParser: DateParser,
private val tracker: CrashAndErrorTracker) : PeopleEventsRepository {
override fun fetchPeopleWithEvents(): List {
val cursor = contentResolver.query(CONTENT_URI, PROJECTION, SELECTION, SELECT_ARGS, SORT_ORDER)
if (isInvalid(cursor)) {
return emptyList()
}
val events = ArrayList()
try {
while (cursor!!.moveToNext()) {
val contactId = getContactIdFrom(cursor)
val eventType = getEventTypeFrom(cursor)
try {
val eventDate = getEventDateFrom(cursor)
val eventId = getEventIdFrom(cursor)
val contact = contactsProvider.getContact(contactId, SOURCE_DEVICE)
events.add(ContactEvent(Optional(eventId), eventType, eventDate, contact))
} catch (e: DateParseException) {
tracker.track(e)
} catch (e: ContactNotFoundException) {
Log.e(e)
}
}
} finally {
cursor!!.close()
}
return events
}
private fun getContactIdFrom(cursor: Cursor): Long {
val contactIdIndex = cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)
return cursor.getLong(contactIdIndex)
}
@Throws(DateParseException::class)
private fun getEventDateFrom(cursor: Cursor): Date {
val dateIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Event.START_DATE)
val dateRaw = cursor.getString(dateIndex)
return dateParser.parse(dateRaw)
}
private fun getEventTypeFrom(cursor: Cursor): EventType {
val eventTypeIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Event.TYPE)
val eventTypeRaw = cursor.getInt(eventTypeIndex)
return when (eventTypeRaw) {
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY -> StandardEventType.BIRTHDAY
ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY -> StandardEventType.ANNIVERSARY
ContactsContract.CommonDataKinds.Event.TYPE_CUSTOM -> StandardEventType.CUSTOM
else -> StandardEventType.OTHER
}
}
private fun isInvalid(cursor: Cursor?): Boolean {
return cursor == null || cursor.isClosed
}
companion object {
private val CONTENT_URI = ContactsContract.Data.CONTENT_URI
private val PROJECTION = arrayOf(
ContactsContract.Data.CONTACT_ID,
ContactsContract.CommonDataKinds.Event.TYPE,
ContactsContract.CommonDataKinds.Event._ID,
ContactsContract.CommonDataKinds.Event.START_DATE
)
private const val SELECTION = (
"( " + ContactsContract.Data.MIMETYPE + " = ? "
+ " AND " + ContactsContract.Data.IN_VISIBLE_GROUP + " = 1)")
private val SELECT_ARGS = arrayOf(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE)
private val SORT_ORDER: String? = null
private fun getEventIdFrom(cursor: Cursor): Long {
val eventIdIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Event._ID)
return cursor.getLong(eventIdIndex)
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/peopleevents/AndroidUpcomingEventSettings.kt
================================================
package com.alexstyl.specialdates.events.peopleevents
import android.content.Context
import com.alexstyl.specialdates.EasyPreferences
import com.alexstyl.specialdates.R
import com.alexstyl.specialdates.events.database.EventSQLiteOpenHelper
class AndroidUpcomingEventSettings(context: Context) : UpcomingEventsSettings {
private val preferences: EasyPreferences = EasyPreferences.createForPrivatePreferences(context, R.string.pref_events)
override fun hasBeenInitialised(): Boolean {
return isUpdatedVersion() && preferences.getBoolean(R.string.key_events_are_initialised, false)
}
private fun isUpdatedVersion(): Boolean {
val lastDatabaseVersion = preferences.getInt(R.string.key_database_version, -1)
return lastDatabaseVersion >= EventSQLiteOpenHelper.DATABASE_VERSION
}
override fun markEventsAsInitialised() {
preferences.setBoolean(R.string.key_events_are_initialised, true)
preferences.setInteger(R.string.key_database_version, EventSQLiteOpenHelper.DATABASE_VERSION)
}
override fun reset() {
preferences.setBoolean(R.string.key_events_are_initialised, false)
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/peopleevents/ContactEventsMarshaller.java
================================================
package com.alexstyl.specialdates.events.peopleevents;
import android.content.ContentValues;
import com.alexstyl.specialdates.Optional;
import com.alexstyl.specialdates.contact.Contact;
import com.alexstyl.specialdates.date.ContactEvent;
import com.alexstyl.specialdates.events.database.DatabaseContract.AnnualEventsContract;
import java.util.List;
public class ContactEventsMarshaller {
private static final int DEFAULT_VALUES_SIZE = 5;
private static final int IS_VISIBILE = 1;
private final ShortDateLabelCreator dateLabelCreator;
public ContactEventsMarshaller(ShortDateLabelCreator dateLabelCreator) {
this.dateLabelCreator = dateLabelCreator;
}
public ContentValues[] marshall(List item) {
ContentValues[] returningValues = new ContentValues[item.size()];
for (int i = 0; i < item.size(); i++) {
ContactEvent event = item.get(i);
returningValues[i] = createValuesFor(event);
}
return returningValues;
}
private ContentValues createValuesFor(ContactEvent event) {
Contact contact = event.getContact();
ContentValues values = new ContentValues(DEFAULT_VALUES_SIZE);
values.put(AnnualEventsContract.CONTACT_ID, contact.getContactID());
values.put(AnnualEventsContract.DISPLAY_NAME, contact.getDisplayName().toString());
values.put(AnnualEventsContract.DATE, dateLabelCreator.createLabelWithYearPreferredFor(event.getDate()));
values.put(AnnualEventsContract.EVENT_TYPE, event.getType().getId());
values.put(AnnualEventsContract.SOURCE, contact.getSource());
values.put(AnnualEventsContract.VISIBLE, IS_VISIBILE);
putDeviceContactIdIfPresent(event, values);
return values;
}
private void putDeviceContactIdIfPresent(ContactEvent event, ContentValues values) {
Optional deviceEventId = event.getDeviceEventId();
if (deviceEventId.isPresent()) {
values.put(AnnualEventsContract.DEVICE_EVENT_ID, deviceEventId.get());
} else {
values.put(AnnualEventsContract.DEVICE_EVENT_ID, -1);
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/peopleevents/CustomEventProvider.kt
================================================
package com.alexstyl.specialdates.events.peopleevents
import android.content.ContentResolver
import android.provider.ContactsContract
class CustomEventProvider(private val resolver: ContentResolver) {
fun getEventWithId(deviceId: Long): EventType {
resolver
.query(CONTENT_URI,
PROJECTION,
SELECTION,
arrayOf(deviceId.toString(), ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE),
SORT_ORDER)
.use { cursor ->
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Event.LABEL)
val eventName = cursor.getString(columnIndex)
return CustomEventType(eventName)
}
}
return StandardEventType.OTHER
}
companion object {
private val CONTENT_URI = ContactsContract.Data.CONTENT_URI
private val PROJECTION = arrayOf(ContactsContract.CommonDataKinds.Event.LABEL)
private const val SELECTION = (
"( " + ContactsContract.Data._ID + " = ?"
+ " AND " + ContactsContract.Data.MIMETYPE + " = ? "
+ " AND " + ContactsContract.Data.IN_VISIBLE_GROUP + " = 1"
+ ")")
private const val SORT_ORDER = ContactsContract.CommonDataKinds.Event._ID + " LIMIT 1"
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/events/peopleevents/PeopleEventsModule.kt
================================================
package com.alexstyl.specialdates.events.peopleevents
import android.appwidget.AppWidgetManager
import android.content.ContentResolver
import android.content.Context
import com.alexstyl.specialdates.CrashAndErrorTracker
import com.alexstyl.specialdates.contact.ContactsProvider
import com.alexstyl.specialdates.date.DateParser
import com.alexstyl.specialdates.events.SettingsPresenter
import com.alexstyl.specialdates.events.database.EventSQLiteOpenHelper
import com.alexstyl.specialdates.events.namedays.NamedayDatabaseRefresher
import com.alexstyl.specialdates.events.namedays.NamedayUserSettings
import com.alexstyl.specialdates.events.namedays.calendar.resource.NamedayCalendarProvider
import com.alexstyl.specialdates.upcoming.widget.list.UpcomingEventsScrollingWidgetView
import com.alexstyl.specialdates.upcoming.widget.today.TodayUpcomingEventsView
import com.alexstyl.specialdates.wear.WearSyncUpcomingEventsView
import dagger.Module
import dagger.Provides
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import javax.inject.Singleton
@Module
@Singleton
class PeopleEventsModule(private val context: Context) {
@Provides
fun peopleEventsProvider(peopleDynamicNamedaysProvider: PeopleDynamicNamedaysProvider,
androidPeopleEventsProvider: AndroidPeopleEventsProvider): PeopleEventsProvider {
return CompositePeopleEventsProvider(listOf(peopleDynamicNamedaysProvider, androidPeopleEventsProvider))
}
@Provides
fun androidPeopleEventsProvider(sqLiteOpenHelper: EventSQLiteOpenHelper,
contactsProvider: ContactsProvider,
dateParser: DateParser,
tracker: CrashAndErrorTracker,
shortLabelCreator: ShortDateLabelCreator): AndroidPeopleEventsProvider {
return AndroidPeopleEventsProvider(
sqLiteOpenHelper,
contactsProvider,
CustomEventProvider(context.contentResolver),
dateParser,
tracker,
shortLabelCreator
)
}
@Provides
fun peopleNamedayCalculator(namedayPreferences: NamedayUserSettings,
namedaysCalendarProvider: NamedayCalendarProvider,
contactsProvider: ContactsProvider): PeopleDynamicNamedaysProvider {
return PeopleDynamicNamedaysProvider(namedayPreferences, namedaysCalendarProvider, contactsProvider)
}
@Provides
@Singleton
fun peopleEventsViewRefresher(appContext: Context, appWidgetManager: AppWidgetManager): UpcomingEventsViewRefresher {
return UpcomingEventsViewRefresher(mutableSetOf(
WearSyncUpcomingEventsView(appContext),
TodayUpcomingEventsView(appContext, appWidgetManager),
UpcomingEventsScrollingWidgetView(appContext, appWidgetManager)
))
}
@Provides
fun peopleEventsStaticEventsRefresher(
eventSQlite: EventSQLiteOpenHelper,
contentResolver: ContentResolver,
contactsProvider: ContactsProvider,
dateParser: DateParser,
marshaller: ContactEventsMarshaller,
tracker: CrashAndErrorTracker): PeopleEventsStaticEventsRefresher {
val repository = AndroidPeopleEventsRepository(contentResolver, contactsProvider, dateParser, tracker)
val androidPeopleEventsPersister = AndroidPeopleEventsPersister(eventSQlite, marshaller, tracker)
return PeopleEventsStaticEventsRefresher(repository, androidPeopleEventsPersister)
}
@Provides
fun namedayDatabaseRefresher(namedayUserSettings: NamedayUserSettings,
databaseProvider: PeopleEventsPersister,
provider: PeopleDynamicNamedaysProvider): NamedayDatabaseRefresher {
return NamedayDatabaseRefresher(namedayUserSettings, databaseProvider, provider)
}
@Provides
fun peopleEventsUpdater(staticRefresher: PeopleEventsStaticEventsRefresher,
namedayRefresher: NamedayDatabaseRefresher,
viewRefresher: UpcomingEventsViewRefresher,
settings: UpcomingEventsSettings): PeopleEventsUpdater {
return PeopleEventsUpdater(
staticRefresher,
namedayRefresher,
viewRefresher,
settings,
Schedulers.io(),
AndroidSchedulers.mainThread()
)
}
@Provides
fun marshaller(dateLabelCreator: ShortDateLabelCreator) = ContactEventsMarshaller(dateLabelCreator)
@Provides
fun peopleEventsPersister(tracker: CrashAndErrorTracker,
marshaller: ContactEventsMarshaller,
helper: EventSQLiteOpenHelper): PeopleEventsPersister {
return AndroidPeopleEventsPersister(helper, marshaller, tracker)
}
@Provides
fun eventPreferences(): UpcomingEventsSettings {
return AndroidUpcomingEventSettings(context)
}
@Provides
fun peopleEventsDatabaseUpdater(uiRefresher: UpcomingEventsViewRefresher, peopleEventsUpdater: PeopleEventsUpdater): SettingsPresenter {
return SettingsPresenter(peopleEventsUpdater, uiRefresher, Schedulers.io())
}
@Provides
fun shortDateCreator() = ShortDateLabelCreator()
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/AndroidFacebookPreferences.java
================================================
package com.alexstyl.specialdates.facebook;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.R;
public final class AndroidFacebookPreferences implements FacebookUserSettings {
private final EasyPreferences preferences;
AndroidFacebookPreferences(EasyPreferences preferences) {
this.preferences = preferences;
}
@Override
public void store(UserCredentials userCredentials) {
preferences.setLong(R.string.key_facebook_user_id, userCredentials.getUid());
preferences.setString(R.string.key_facebook_user_key, userCredentials.getKey());
preferences.setString(R.string.key_facebook_user_name, userCredentials.getName());
}
@Override
public UserCredentials retrieveCredentials() {
long uid = preferences.getLong(R.string.key_facebook_user_id, -1);
String key = preferences.getString(R.string.key_facebook_user_key, "");
String name = preferences.getString(R.string.key_facebook_user_name, "");
return new UserCredentials(uid, key, name);
}
@Override
public boolean isLoggedIn() {
return !UserCredentials.ANNONYMOUS.equals(retrieveCredentials());
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/FacebookLogoutService.java
================================================
package com.alexstyl.specialdates.facebook;
import com.alexstyl.specialdates.events.peopleevents.UpcomingEventsViewRefresher;
import com.alexstyl.specialdates.facebook.friendimport.FacebookFriendsPersister;
import io.reactivex.Completable;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Action;
import io.reactivex.schedulers.Schedulers;
class FacebookLogoutService {
private final UpcomingEventsViewRefresher refresher;
private final FacebookUserSettings preferences;
private final FacebookFriendsPersister persister;
private final Scheduler resultScheduler;
private final OnFacebookLogOutCallback callback;
private CompositeDisposable disposable = new CompositeDisposable();
FacebookLogoutService(Scheduler resultScheduler,
FacebookUserSettings preferences,
FacebookFriendsPersister persister,
UpcomingEventsViewRefresher refresher, OnFacebookLogOutCallback callback) {
this.resultScheduler = resultScheduler;
this.preferences = preferences;
this.persister = persister;
this.refresher = refresher;
this.callback = callback;
}
void logOut() {
disposable.add(
Completable.fromAction(clearAllUserPresence())
.doOnComplete(onLogOut())
.observeOn(resultScheduler)
.doOnComplete(refreshAllUI())
.subscribeOn(Schedulers.io())
.subscribe()
);
}
private Action refreshAllUI() {
return new Action() {
@Override
public void run() throws Exception {
refresher.refreshViews();
}
};
}
private Action clearAllUserPresence() {
return new Action() {
@Override
public void run() throws Exception {
preferences.store(UserCredentials.ANNONYMOUS);
persister.removeAllFriends();
}
};
}
private Action onLogOut() {
return new Action() {
@Override
public void run() throws Exception {
callback.onUserLogOut();
}
};
}
void dispose() {
disposable.dispose();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/FacebookModule.java
================================================
package com.alexstyl.specialdates.facebook;
import android.content.Context;
import com.alexstyl.specialdates.EasyPreferences;
import com.alexstyl.specialdates.R;
import dagger.Module;
import dagger.Provides;
@Module
public class FacebookModule {
private final Context context;
public FacebookModule(Context context) {
this.context = context;
}
@Provides
FacebookUserSettings userSettings() {
return new AndroidFacebookPreferences(EasyPreferences.createForPrivatePreferences(context, R.string.pref_facebook));
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/FacebookProfileActivity.java
================================================
package com.alexstyl.specialdates.facebook;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.alexstyl.specialdates.AppComponent;
import com.alexstyl.specialdates.CrashAndErrorTracker;
import com.alexstyl.specialdates.ExternalNavigator;
import com.alexstyl.specialdates.MementoApplication;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.analytics.Analytics;
import com.alexstyl.specialdates.analytics.Screen;
import com.alexstyl.specialdates.events.database.EventSQLiteOpenHelper;
import com.alexstyl.specialdates.events.peopleevents.ContactEventsMarshaller;
import com.alexstyl.specialdates.events.peopleevents.AndroidPeopleEventsPersister;
import com.alexstyl.specialdates.events.peopleevents.UpcomingEventsViewRefresher;
import com.alexstyl.specialdates.events.peopleevents.UpcomingEventsSettings;
import com.alexstyl.specialdates.facebook.friendimport.FacebookFriendsPersister;
import com.alexstyl.specialdates.images.ImageLoader;
import com.alexstyl.specialdates.ui.base.ThemedMementoActivity;
import com.alexstyl.specialdates.ui.widget.MementoToolbar;
import javax.inject.Inject;
import java.net.URI;
import io.reactivex.android.schedulers.AndroidSchedulers;
import static com.novoda.notils.caster.Views.findById;
public class FacebookProfileActivity extends ThemedMementoActivity implements FacebookProfileView {
private static final int LOGOUT_ID = 40444;
private ExternalNavigator navigator;
private FacebookProfilePresenter presenter;
private ImageView profilePicture;
private TextView userName;
@Inject Analytics analytics;
@Inject ImageLoader imageLoader;
@Inject UpcomingEventsViewRefresher uiRefresher;
@Inject CrashAndErrorTracker tracker;
@Inject FacebookUserSettings facebookSettings;
@Inject UpcomingEventsSettings eventsSettings;
@Inject ContactEventsMarshaller marshaller;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppComponent applicationModule = ((MementoApplication) getApplication()).getApplicationModule();
applicationModule.inject(this);
analytics.trackScreen(Screen.FACEBOOK_PROFILE);
setContentView(R.layout.activity_facebook_profile);
setupToolbar();
profilePicture = findById(this, R.id.facebook_profile_avatar);
userName = findById(this, R.id.facebook_profile_name);
findById(this, R.id.facebook_profile_fb_page).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
navigator.toFacebookPage();
}
});
FacebookFriendsPersister persister = new FacebookFriendsPersister(
new AndroidPeopleEventsPersister(new EventSQLiteOpenHelper(this), marshaller, tracker));
navigator = new ExternalNavigator(this, analytics, tracker);
FacebookLogoutService service = new FacebookLogoutService(
AndroidSchedulers.mainThread(),
facebookSettings,
persister,
uiRefresher,
onLogOut()
);
presenter = new FacebookProfilePresenter(
service,
this,
facebookSettings
);
presenter.startPresenting();
}
private OnFacebookLogOutCallback onLogOut() {
return new OnFacebookLogOutCallback() {
@Override
public void onUserLogOut() {
finish();
}
};
}
private void setupToolbar() {
MementoToolbar toolbar = findById(this, R.id.memento_toolbar);
setSupportActionBar(toolbar);
toolbar.displayNavigationIconAsClose();
setTitle(null);
}
@Override
public void display(UserCredentials userCredentials) {
userName.setText(userCredentials.getName());
URI uri = FacebookImagePath.INSTANCE.forUid(userCredentials.getUid());
imageLoader
.load(uri)
.asCircle()
.into(profilePicture);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, LOGOUT_ID, 0, R.string.log_out);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == LOGOUT_ID) {
presenter.logOut();
analytics.trackFacebookLoggedOut();
return true;
} else if (itemId == android.R.id.home) {
finish();
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
super.onDestroy();
presenter.stopPresenting();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/FacebookProfilePresenter.java
================================================
package com.alexstyl.specialdates.facebook;
class FacebookProfilePresenter {
private final FacebookLogoutService service;
private final FacebookProfileView view;
private final FacebookUserSettings settings;
FacebookProfilePresenter(FacebookLogoutService service,
FacebookProfileView view,
FacebookUserSettings settings) {
this.service = service;
this.view = view;
this.settings = settings;
}
void startPresenting() {
UserCredentials userCredentials = settings.retrieveCredentials();
view.display(userCredentials);
}
void logOut() {
service.logOut();
}
void stopPresenting() {
service.dispose();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/FacebookProfileView.java
================================================
package com.alexstyl.specialdates.facebook;
interface FacebookProfileView {
void display(UserCredentials userCredentials);
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/OnFacebookLogOutCallback.java
================================================
package com.alexstyl.specialdates.facebook;
interface OnFacebookLogOutCallback {
void onUserLogOut();
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/ScreenOrientationLock.java
================================================
package com.alexstyl.specialdates.facebook;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.view.Surface;
import android.view.WindowManager;
public class ScreenOrientationLock {
public void lock(Activity activity) {
int orientation = activity.getRequestedOrientation();
int rotation = ((WindowManager)
activity.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay().getRotation();
switch (rotation) {
case Surface.ROTATION_0:
orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
break;
case Surface.ROTATION_90:
orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
break;
case Surface.ROTATION_180:
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
break;
default:
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
break;
}
activity.setRequestedOrientation(orientation);
}
public void unlock(Activity activity) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/friendimport/CalendarURLCreator.java
================================================
package com.alexstyl.specialdates.facebook.friendimport;
import com.alexstyl.specialdates.CrashAndErrorTracker;
import com.alexstyl.specialdates.facebook.UserCredentials;
import java.net.MalformedURLException;
import java.net.URL;
class CalendarURLCreator {
private final CrashAndErrorTracker tracker;
CalendarURLCreator(CrashAndErrorTracker tracker) {
this.tracker = tracker;
}
URL createFrom(UserCredentials user) {
try {
return new URL("https://www.facebook.com/ical/b.php?locale=en_US&uid=" + user.getUid() + "&key=" + user.getKey());
} catch (MalformedURLException e) {
tracker.track(e);
throw new IllegalArgumentException(e);
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/friendimport/FacebookFriendsIntentService.java
================================================
package com.alexstyl.specialdates.facebook.friendimport;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Intent;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import com.alexstyl.specialdates.BuildConfig;
import com.alexstyl.specialdates.CrashAndErrorTracker;
import com.alexstyl.specialdates.MementoApplication;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.date.ContactEvent;
import com.alexstyl.specialdates.date.DateParser;
import com.alexstyl.specialdates.events.database.EventSQLiteOpenHelper;
import com.alexstyl.specialdates.events.peopleevents.AndroidPeopleEventsPersister;
import com.alexstyl.specialdates.events.peopleevents.ContactEventsMarshaller;
import com.alexstyl.specialdates.events.peopleevents.UpcomingEventsViewRefresher;
import com.alexstyl.specialdates.facebook.FacebookUserSettings;
import com.alexstyl.specialdates.facebook.UserCredentials;
import javax.inject.Inject;
import java.net.URL;
import java.util.List;
public class FacebookFriendsIntentService extends IntentService {
private static final String TAG = FacebookFriendsIntentService.class.getSimpleName();
private static final int NOTIFICATION_ID = 123;
@Inject UpcomingEventsViewRefresher uiRefresher;
@Inject CrashAndErrorTracker tracker;
@Inject FacebookUserSettings facebookUserSettings;
@Inject DateParser parser;
@Inject ContactEventsMarshaller marshaller;
public FacebookFriendsIntentService() {
super(TAG);
}
@Override
public void onCreate() {
super.onCreate();
((MementoApplication) getApplication()).getApplicationModule().inject(this);
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
FacebookCalendarLoader calendarLoader = new FacebookCalendarLoader();
FacebookContactFactory factory = new FacebookContactFactory(parser);
ContactEventSerialiser serialiser = new ContactEventSerialiser(factory, tracker);
FacebookBirthdaysProvider calendarFetcher = new FacebookBirthdaysProvider(calendarLoader, serialiser);
UserCredentials userCredentials = facebookUserSettings.retrieveCredentials();
if (isAnnonymous(userCredentials)) {
tracker.track(new RuntimeException("Tried to fetch events, but was anonymous"));
return;
}
CalendarURLCreator calendarURLCreator = new CalendarURLCreator(tracker);
URL calendarUrl = calendarURLCreator.createFrom(userCredentials);
FacebookFriendsPersister persister = new FacebookFriendsPersister(
new AndroidPeopleEventsPersister(
new EventSQLiteOpenHelper(this),
marshaller,
tracker
)
);
try {
List friends = calendarFetcher.fetchCalendarFrom(calendarUrl);
persister.keepOnly(friends);
uiRefresher.refreshViews();
} catch (CalendarFetcherException e) {
tracker.track(e);
}
if (BuildConfig.DEBUG) {
notifyServiceRan();
}
}
private void notifyServiceRan() {
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("Friends fetched")
.setSmallIcon(R.mipmap.ic_launcher)
.build();
((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification);
}
private boolean isAnnonymous(UserCredentials userCredentials) {
return UserCredentials.ANNONYMOUS.equals(userCredentials);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/friendimport/FacebookFriendsPersister.java
================================================
package com.alexstyl.specialdates.facebook.friendimport;
import com.alexstyl.specialdates.date.ContactEvent;
import com.alexstyl.specialdates.events.peopleevents.PeopleEventsPersister;
import java.util.List;
import static com.alexstyl.specialdates.contact.ContactSource.SOURCE_FACEBOOK;
public final class FacebookFriendsPersister {
private final PeopleEventsPersister persister;
public FacebookFriendsPersister(PeopleEventsPersister persister) {
this.persister = persister;
}
void keepOnly(List friends) {
persister.deleteAllEventsOfSource(SOURCE_FACEBOOK);
persister.insertAnnualEvents(friends);
}
public void removeAllFriends() {
persister.deleteAllEventsOfSource(SOURCE_FACEBOOK);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/friendimport/FacebookFriendsScheduler.java
================================================
package com.alexstyl.specialdates.facebook.friendimport;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import com.alexstyl.specialdates.TimeOfDay;
import com.alexstyl.specialdates.date.Date;
import com.alexstyl.specialdates.date.DateAndTime;
public final class FacebookFriendsScheduler {
private static final TimeOfDay EIGHT_O_CLOCK = new TimeOfDay(8, 0);
private final AlarmManager alarmManager;
private final Context context;
public FacebookFriendsScheduler(Context context, AlarmManager alarmManager) {
this.context = context;
this.alarmManager = alarmManager;
}
public void scheduleNext() {
DateAndTime dateAndTime = new DateAndTime(Date.Companion.today().addDay(1), EIGHT_O_CLOCK);
PendingIntent pi = PendingIntent.getService(
context,
0,
new Intent(context, FacebookFriendsIntentService.class),
PendingIntent.FLAG_UPDATE_CURRENT
);
alarmManager.setRepeating(AlarmManager.RTC, dateAndTime.toMilis(),
AlarmManager.INTERVAL_DAY, pi
);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/login/CookieResetter.java
================================================
package com.alexstyl.specialdates.facebook.login;
import android.webkit.CookieManager;
import android.webkit.ValueCallback;
import com.alexstyl.android.Version;
class CookieResetter {
private final CookieManager cookieManager;
CookieResetter(CookieManager instance) {
this.cookieManager = instance;
}
void clearAll() {
removeAllCookies();
cookieManager.setCookie(".facebook.com", "locale=");
cookieManager.setCookie(".facebook.com", "datr=");
cookieManager.setCookie(".facebook.com", "s=");
cookieManager.setCookie(".facebook.com", "csm=");
cookieManager.setCookie(".facebook.com", "fr=");
cookieManager.setCookie(".facebook.com", "lu=");
cookieManager.setCookie(".facebook.com", "c_user=");
cookieManager.setCookie(".facebook.com", "xs=");
cookieManager.setCookie(".facebook.com", "wd");
cookieManager.setCookie(".facebook.com", "presence");
cookieManager.setCookie(".facebook.com", "act");
cookieManager.setCookie(".facebook.com", "lu");
cookieManager.setCookie(".facebook.com", "pl");
cookieManager.setCookie(".facebook.com", "fr");
cookieManager.setCookie(".facebook.com", "xs");
cookieManager.setCookie(".facebook.com", "c_user");
cookieManager.setCookie(".facebook.com", "sb");
cookieManager.setCookie(".facebook.com", "dats");
cookieManager.setCookie(".facebook.com", "datr");
cookieManager.setCookie(".facebook.com", "locale");
cookieManager.setCookie(".facebook.com", "x-referer");
}
private void removeAllCookies() {
if (Version.INSTANCE.hasLollipop()) {
cookieManager.removeAllCookies(new ValueCallback() {
@Override
public void onReceiveValue(Boolean value) {
}
});
} else {
cookieManager.removeAllCookie();
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/login/CredentialsExtractor.java
================================================
package com.alexstyl.specialdates.facebook.login;
import com.alexstyl.specialdates.facebook.UserCredentials;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class CredentialsExtractor {
private static final Pattern BIRTHDAY_PATTERN = Pattern.compile("(www.facebook.com/ical/b.php\\?uid=\\w+&key=.+?(?=\"))");
private static final String UID = "uid=";
private static final int UID_LENGTH = UID.length();
private static final String KEY = "key=";
private static final int KEY_LENGTH = KEY.length();
private static final int USER_DETAILS = 1;
private static final String OPENNING_SPAN = "";
private static final String CLOSING_SPAN = "";
UserCredentials extractCalendarURL(String pageSource) {
Matcher matcher = BIRTHDAY_PATTERN.matcher(pageSource);
if (matcher.find()) {
String url = matcher
.group(1)
.replace("&", "&");
String name = obtainName(pageSource);
return createFrom(url, name);
} else {
return UserCredentials.ANNONYMOUS;
}
}
static UserCredentials createFrom(String calendarURL, String name) {
int indexOfKey = calendarURL.indexOf(KEY);
int indexOfUserID = calendarURL.indexOf(UID);
int indexOfEnd = calendarURL.indexOf("&", indexOfUserID);
long userID = Long.parseLong(calendarURL.substring(indexOfUserID + UID_LENGTH, indexOfEnd));
String key = calendarURL.substring(indexOfKey + KEY_LENGTH);
return new UserCredentials(userID, key, name);
}
private String obtainName(String pageSource) {
try {
String userDetails = pageSource.split("data-testid=\"blue_bar_profile_link\">")[USER_DETAILS];
int startOfName = userDetails.indexOf(OPENNING_SPAN) + OPENNING_SPAN.length();
int endOfName = userDetails.indexOf(CLOSING_SPAN);
return userDetails.substring(startOfName, endOfName);
} catch (Exception e) {
return "";
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/login/FBImportClient.java
================================================
package com.alexstyl.specialdates.facebook.login;
import android.graphics.Bitmap;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
class FBImportClient extends WebViewClient {
private static final String HTTPS = "https://";
private static final String HTTP = "http://";
private static final String MOBILE_HOME = "m.facebook.com/home.php";
private static final String DESKTOP_HOME = "www.facebook.com/home.php";
private static final String DESKTOP_USER_AGENT = "Mozilla/5.0 (X11; U; Linux i686; en US; rv:1.9.0.4) Gecko/20100101 Firefox/4.0";
private static final String URL_WITH_BIRTHDAY_SOURCE = "https://www.facebook.com/events/calendar";
private final WebView webView;
private FacebookLogInCallback listener;
FBImportClient(WebView webview) {
this.webView = webview;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
if (isHomePage(url)) {
view.stopLoading();
internalOnUserLoggedIn();
}
}
private boolean isHomePage(String url) {
return url.startsWith(HTTP + MOBILE_HOME) || url.startsWith(HTTPS + MOBILE_HOME)
|| url.startsWith(HTTP + DESKTOP_HOME) || url.startsWith(HTTPS + DESKTOP_HOME);
}
private void internalOnUserLoggedIn() {
switchToDesktopBrowsing();
webView.loadUrl(URL_WITH_BIRTHDAY_SOURCE);
listener.onUserCredentialsSubmitted();
}
private void switchToDesktopBrowsing() {
WebSettings settings = webView.getSettings();
settings.setUserAgentString(DESKTOP_USER_AGENT);
}
@Override
public void onPageFinished(WebView view, String url) {
if (url.contains(URL_WITH_BIRTHDAY_SOURCE)) {
webView.loadUrl("javascript:window.HTMLOUT.processHTML(''+document.getElementsByTagName('html')[0].innerHTML+'');");
}
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
listener.onError(new FacebookLogInException(description));
}
public void setListener(FacebookLogInCallback listener) {
this.listener = listener;
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/login/FacebookImportView.java
================================================
package com.alexstyl.specialdates.facebook.login;
import com.alexstyl.specialdates.facebook.UserCredentials;
interface FacebookImportView {
void showLoading();
void showData(UserCredentials user);
void showError();
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/login/FacebookLogInActivity.java
================================================
package com.alexstyl.specialdates.facebook.login;
import android.app.AlarmManager;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.BounceInterpolator;
import android.webkit.CookieManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.alexstyl.specialdates.AppComponent;
import com.alexstyl.specialdates.CrashAndErrorTracker;
import com.alexstyl.specialdates.MementoApplication;
import com.alexstyl.specialdates.R;
import com.alexstyl.specialdates.ShareAppIntentCreator;
import com.alexstyl.specialdates.Strings;
import com.alexstyl.specialdates.analytics.Analytics;
import com.alexstyl.specialdates.analytics.Screen;
import com.alexstyl.specialdates.facebook.FacebookImagePath;
import com.alexstyl.specialdates.facebook.FacebookUserSettings;
import com.alexstyl.specialdates.facebook.ScreenOrientationLock;
import com.alexstyl.specialdates.facebook.UserCredentials;
import com.alexstyl.specialdates.facebook.friendimport.FacebookFriendsIntentService;
import com.alexstyl.specialdates.facebook.friendimport.FacebookFriendsScheduler;
import com.alexstyl.specialdates.images.ImageLoader;
import com.alexstyl.specialdates.ui.base.ThemedMementoActivity;
import com.novoda.notils.caster.Views;
import com.novoda.notils.meta.AndroidUtils;
import javax.inject.Inject;
import java.net.URI;
public class FacebookLogInActivity extends ThemedMementoActivity implements FacebookImportView {
private FacebookFriendsScheduler facebookFriendsScheduler;
private FacebookWebView webView;
private ImageView avatar;
private TextView helloView;
private TextView moreText;
private ScreenOrientationLock orientationLock;
private ProgressBar progress;
private Button shareButton;
private Button closeButton;
@Inject Analytics analytics;
@Inject Strings stringResource;
@Inject CrashAndErrorTracker tracker;
@Inject ImageLoader imageLoader;
@Inject FacebookUserSettings facebookUserSettings;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppComponent applicationModule = ((MementoApplication) getApplication()).getApplicationModule();
applicationModule.inject(this);
analytics.trackScreen(Screen.FACEBOOK_LOG_IN);
setContentView(R.layout.activity_facebook_log_in);
Toolbar toolbar = Views.findById(this, R.id.memento_toolbar);
setSupportActionBar(toolbar);
avatar = Views.findById(this, R.id.facebook_import_avatar);
helloView = Views.findById(this, R.id.facebook_import_hello);
moreText = Views.findById(this, R.id.facebook_import_description);
progress = Views.findById(this, R.id.progress);
shareButton = Views.findById(this, R.id.facebook_import_share);
shareButton.setOnClickListener(shareAppIntentOnClick());
closeButton = Views.findById(this, R.id.facebook_import_close);
closeButton.setOnClickListener(onCloseButtonPressed());
webView = Views.findById(this, R.id.facebook_import_webview);
orientationLock = new ScreenOrientationLock();
facebookFriendsScheduler = new FacebookFriendsScheduler(
thisActivity(),
(AlarmManager) getSystemService(ALARM_SERVICE)
);
webView.setCallback(facebookCallback);
UserCredentials userCredentials = facebookUserSettings.retrieveCredentials();
if (savedInstanceState == null || userCredentials.equals(UserCredentials.ANNONYMOUS)) {
new CookieResetter(CookieManager.getInstance()).clearAll();
webView.loadLogInPage();
} else {
showData(userCredentials);
}
}
private View.OnClickListener onCloseButtonPressed() {
return new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
};
}
private View.OnClickListener shareAppIntentOnClick() {
return new View.OnClickListener() {
@Override
public void onClick(View v) {
ShareAppIntentCreator appIntentCreator = new ShareAppIntentCreator(stringResource);
Intent intent = appIntentCreator.buildIntent();
startActivity(intent);
analytics.trackAppInviteRequested();
}
};
}
private final FacebookLogInCallback facebookCallback = new FacebookLogInCallback() {
@Override
public void onUserCredentialsSubmitted() {
AndroidUtils.requestHideKeyboard(thisActivity(), webView);
showLoading();
}
@Override
public void onUserLoggedIn(UserCredentials credentials) {
fetchFacebookFriends();
showData(credentials);
analytics.trackFacebookLoggedIn();
}
private void fetchFacebookFriends() {
facebookFriendsScheduler.scheduleNext();
startFacebookFetchService();
}
private void startFacebookFetchService() {
Intent intent = new Intent(thisActivity(), FacebookFriendsIntentService.class);
startService(intent);
}
@Override
public void onError(Exception e) {
showError();
tracker.track(e);
}
};
@Override
public void showLoading() {
orientationLock.lock(thisActivity());
progress.setVisibility(View.VISIBLE);
webView.setVisibility(View.GONE);
avatar.setVisibility(View.GONE);
helloView.setVisibility(View.GONE);
moreText.setVisibility(View.GONE);
closeButton.setVisibility(View.GONE);
shareButton.setVisibility(View.GONE);
}
@Override
public void showData(UserCredentials userCredentials) {
progress.setVisibility(View.GONE);
webView.setVisibility(View.GONE);
avatar.setVisibility(View.VISIBLE);
helloView.setVisibility(View.VISIBLE);
moreText.setVisibility(View.VISIBLE);
closeButton.setVisibility(View.VISIBLE);
shareButton.setVisibility(View.VISIBLE);
URI uri = FacebookImagePath.INSTANCE.forUid(userCredentials.getUid());
imageLoader
.load(uri)
.asCircle()
.into(avatar);
animateAvatarWithBounce();
avatar.setVisibility(View.VISIBLE);
String name = userCredentials.getName();
if (name.isEmpty()) {
helloView.setText(R.string.Welcome);
} else {
helloView.setText(getString(R.string.facebook_hi, name));
}
}
private void animateAvatarWithBounce() {
final Animation animation = AnimationUtils.loadAnimation(FacebookLogInActivity.this, R.anim.bounce);
animation.setInterpolator(new BounceInterpolator());
avatar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
avatar.startAnimation(animation);
analytics.trackOnAvatarBounce();
}
});
avatar.startAnimation(animation);
}
@Override
public void showError() {
avatar.setVisibility(View.VISIBLE);
avatar.setImageResource(R.drawable.ic_facebook_sad);
helloView.setVisibility(View.VISIBLE);
helloView.setText(R.string.facebook_error);
closeButton.setVisibility(View.VISIBLE);
moreText.setVisibility(View.VISIBLE);
moreText.setText(R.string.facebook_try_again);
progress.setVisibility(View.GONE);
webView.setVisibility(View.GONE);
shareButton.setVisibility(View.GONE);
}
@Override
protected void onDestroy() {
super.onDestroy();
webView.destroy();
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/login/FacebookLogInCallback.java
================================================
package com.alexstyl.specialdates.facebook.login;
import com.alexstyl.specialdates.facebook.UserCredentials;
interface FacebookLogInCallback {
void onUserLoggedIn(UserCredentials loggedInCredentials);
/**
* Called when the user has successfully submitted their log in credentials.
*/
void onUserCredentialsSubmitted();
void onError(Exception e);
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/login/FacebookLogInException.java
================================================
package com.alexstyl.specialdates.facebook.login;
final class FacebookLogInException extends Exception {
FacebookLogInException(String message) {
super(message);
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/login/FacebookWebView.java
================================================
package com.alexstyl.specialdates.facebook.login;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import com.alexstyl.specialdates.MementoApplication;
import com.alexstyl.specialdates.facebook.FacebookUserSettings;
import javax.inject.Inject;
public class FacebookWebView extends WebView {
private FacebookLogInCallback callback;
private FBImportClient client;
@Inject FacebookUserSettings facebookUserSettings;
public FacebookWebView(Context context, AttributeSet attrs) {
super(context, attrs);
setup();
}
public FacebookWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup();
}
private void setup() {
((MementoApplication) getContext().getApplicationContext()).getApplicationModule().inject(this);
clearCache(false);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
WebSettings settings = getSettings();
settings.setJavaScriptEnabled(true);
addJavascriptInterface(new FacebookJavaScriptInterface(), "HTMLOUT");
client = new FBImportClient(this);
setWebViewClient(client);
}
public void loadLogInPage() {
loadUrl("https://m.facebook.com/login");
}
public void setCallback(FacebookLogInCallback callback) {
this.callback = callback;
client.setListener(callback);
}
private class FacebookJavaScriptInterface {
private UserCredentialsExtractorTask userCredentialsExtractorTask;
@JavascriptInterface
@SuppressWarnings("unused")
public void processHTML(String html) {
if (userCredentialsExtractorTask == null) {
userCredentialsExtractorTask = new UserCredentialsExtractorTask(html, facebookUserSettings, callback);
userCredentialsExtractorTask.execute();
}
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/facebook/login/UserCredentialsExtractorTask.java
================================================
package com.alexstyl.specialdates.facebook.login;
import android.os.AsyncTask;
import com.alexstyl.specialdates.facebook.FacebookUserSettings;
import com.alexstyl.specialdates.facebook.UserCredentials;
import javax.security.auth.login.LoginException;
class UserCredentialsExtractorTask extends AsyncTask {
private final CredentialsExtractor extractor = new CredentialsExtractor();
private final String pageSource;
private final FacebookLogInCallback callback;
private final FacebookUserSettings facebookUserSettings;
UserCredentialsExtractorTask(String pageSource, FacebookUserSettings facebookUserSettings, FacebookLogInCallback callback) {
this.pageSource = pageSource;
this.facebookUserSettings = facebookUserSettings;
this.callback = callback;
}
@Override
protected UserCredentials doInBackground(Void... params) {
UserCredentials userCredentials = extractor.extractCalendarURL(pageSource);
if (userCredentials != UserCredentials.ANNONYMOUS) {
facebookUserSettings.store(userCredentials);
}
return userCredentials;
}
@Override
protected void onPostExecute(UserCredentials userCredentials) {
if (userCredentials.isAnnonymous()) {
callback.onError(new LoginException("Couldn't find extract calendar"));
} else {
callback.onUserLoggedIn(userCredentials);
}
}
}
================================================
FILE: android_mobile/src/main/java/com/alexstyl/specialdates/home/DonationBannerView.kt
================================================
package com.alexstyl.specialdates.home
import android.content.Context
import android.graphics.Color
import android.support.v7.widget.LinearLayoutCompat
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout
import com.alexstyl.specialdates.BuildConfig
import com.alexstyl.specialdates.R
import com.google.android.gms.ads.AdListener
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdView
class DonationBannerView(context: Context, attrs: AttributeSet?) : LinearLayoutCompat(context, attrs) {
private var closeView: View? = null
private var listener: OnCloseBannerListener? = null
override fun onFinishInflate() {
super.onFinishInflate()
LayoutInflater.from(context).inflate(R.layout.merge_donation_banner_view, this, true)
super.setOrientation(LinearLayout.HORIZONTAL)
setBackgroundColor(Color.WHITE)
val adView = findViewById(R.id.banner_ad)
adView.adListener = object : AdListener() {
override fun onAdFailedToLoad(p0: Int) {
showAsCallToAction()
}
}
adView.loadAd(adRequest())
closeView = findViewById(R.id.banner_close)
closeView!!.setOnClickListener { listener?.onCloseButtonPressed() }
}
private fun showAsCallToAction() {
val supportButton = findViewById