/* * Copyright 2015. Appsi Mobile * * 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.appsimobile.appsii.module.appsiagenda; import android.Manifest; import android.animation.Animator; import android.animation.LayoutTransition; import android.content.ActivityNotFoundException; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.Loader; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.provider.CalendarContract; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.format.Time; import android.util.SparseBooleanArray; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import com.appsimobile.appsii.AnalyticsManager; import com.appsimobile.appsii.AnimatorAdapter; import com.appsimobile.appsii.DrawableCompat; import com.appsimobile.appsii.ExpandCollapseDrawable; import com.appsimobile.appsii.LoaderManager; import com.appsimobile.appsii.PageController; import com.appsimobile.appsii.PermissionDeniedException; import com.appsimobile.appsii.R; import com.appsimobile.appsii.dagger.AppsiInjector; import com.appsimobile.appsii.module.PermissionHelper; import com.appsimobile.appsii.module.SpacingItemDecoration; import com.appsimobile.appsii.module.ToolbarScrollListener; import com.appsimobile.appsii.permissions.PermissionUtils; import com.appsimobile.util.CollectionUtils; import com.appsimobile.util.TimeUtils; import com.crashlytics.android.Crashlytics; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Formatter; import java.util.List; import java.util.Locale; import java.util.TimeZone; import javax.inject.Inject; /** * Created by nick on 25/05/14. */ public class AgendaController extends PageController implements View.OnClickListener, LayoutTransition.TransitionListener, Toolbar.OnMenuItemClickListener, PermissionHelper.PermissionListener { // TODO: use View.setOnSystemUiVisibilityChangeListener(); to detect system ui-changes /** * The key in the preferences used to store the adapter type */ public static final String PREF_USE_EXPANDED_ADAPTER = "agenda_use_expanded_adapter"; /** * A unique loader is for the agenda events */ private static final int AGENDA_LOADER_ID = 21001; /** * A unique loader id for the event-days */ private static final int AGENDA_EVENT_DAYS_LOADER_ID = 21002; private static final int[] ATTRS = new int[]{ R.attr.colorToolbarTitleText, R.attr.appsiSidebarBackground, }; /** * The height of the header container. This is set through a dimension. * This dimension is used here as well to get the height when the view * is being initialized. */ final int mHeaderWrapperHeight; /** * A formatter that can be used to format date times using DateUtils */ private final Formatter mFormatter; /** * The StringBuilder used by mFormatter. Will contain the result of the * formatter after formatting */ private final StringBuilder mStringBuilder; /** * A temporary time object used to perform time calculations with */ private final Time mTime = new Time(Time.TIMEZONE_UTC); /** * Saved in the preferences, used to determine the adapter to use */ boolean mUseExpandableAdapter; /** * The recycler-view containing the agenda month-browser */ RecyclerView mAgendaRecycler; /** * The recycler-view containing the agenda-items */ RecyclerView mHeaderView; /** * The adapter set on the AgendaListView */ AgendaAdapter mAgendaAdapter; /** * A text-view that is added to the toolbar to allow the user to toggle * the date-picker */ TextView mToolbarTitle; /** * The toolbar with the available actions and toggle for opening the month-browser */ Toolbar mToolbar; /** * The drawable added to the fake title text-view that animates in and * out as the view is collapsed / expanded */ ExpandCollapseDrawable mTitleCompoundDrawable; /** * True when the header is expanded */ boolean mHeaderExpanded; /** * The complete container, wrapping the date-picker and the divider */ View mHeaderContainer; /** * The controller object for the date-picker. Tracks the state and the possible start/ * end dates for the date-picker */ DatePickerControllerImpl mDatePickerController; final Animator.AnimatorListener mCloseListener = new AnimatorAdapter() { @Override public void onAnimationEnd(Animator animation) { mHeaderContainer.setVisibility(View.INVISIBLE); mAgendaRecycler.setBackground(null); updateToolbarTitleAndMonthPosition(); } }; int mAppsiBackgroundColor; SpacingItemDecoration mSpacingDecoration; WeekNumberDecoration mWeekNumberDecoration; List<AgendaEvent> mAgendaEvents; OnClickListener mOnClickListener; int mLastVisiblePosition; ViewGroup mPermissionOverlay; boolean mPendingPermissionError; /** * The shared-preferences we can get the configuration from */ @Inject SharedPreferences mPreferences; @Inject PermissionUtils mPermissionUtils; private SimpleMonthAdapter2 mSimpleMonthAdapter; public AgendaController(Context context, String title) { super(context, title); mStringBuilder = new StringBuilder(50); mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); mHeaderWrapperHeight = context.getResources(). getDimensionPixelSize(R.dimen.agenda_header_wrapper_height); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.page_agenda, container, false); } @Override protected void onViewDestroyed(View view) { } @Override public void onViewCreated(final View view, Bundle savedInstanceState) { mAgendaRecycler = (RecyclerView) view.findViewById(R.id.agenda_recycler_view); mAgendaRecycler.setLayoutManager(new LinearLayoutManager(getContext())); mAgendaRecycler.addOnItemTouchListener(new AgendaOnItemTouchListener()); mPermissionOverlay = (ViewGroup) view.findViewById(R.id.permission_overlay); // setup the header view mHeaderView = (RecyclerView) view.findViewById(R.id.agenda_month_recycler); mHeaderView.setAdapter(mSimpleMonthAdapter); mDatePickerController.registerOnDateChangedListener(new OnDateChangedListener() { @Override public void onDateChanged(int year, int month, int day, boolean fromTap) { scrollAgendaViewToDate(year, month, day, fromTap); } }); mHeaderView.setLayoutManager(new LinearLayoutManager(getContext())); mToolbar = (Toolbar) view.findViewById(R.id.toolbar); mAgendaRecycler.addOnScrollListener(new ToolbarScrollListener(this, mToolbar) { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); View child = recyclerView.getChildAt(0); if (child != null) { int position = recyclerView.getChildLayoutPosition(child); if (position != RecyclerView.NO_POSITION) { syncDayToMonth(position); } } } }); Menu menu = mToolbar.getMenu(); menu.clear(); MenuInflater menuInflater = new MenuInflater(getContext()); menuInflater.inflate(R.menu.page_agenda, menu); // RecyclerView.ItemAnimator animator = new AgendaItemAnimator(); // mAgendaRecycler.setItemAnimator(animator); mAgendaRecycler.setAdapter(mAgendaAdapter); MenuItem menuItemAdd = menu.findItem(R.id.action_new_calendar_event); MenuItem menuItemViewNormal = menu.findItem(R.id.view_type_normal); MenuItem menuItemViewCollapse = menu.findItem(R.id.view_type_collapsable); menuItemAdd.setIcon(R.drawable.ic_add_black_24dp); menuItemViewNormal.setIcon(R.drawable.ic_view_agenda_black_24dp); menuItemViewCollapse.setIcon(R.drawable.ic_view_list_black_24dp); updateTintColors(); mToolbar.setOnMenuItemClickListener(this); mToolbarTitle = (TextView) view.findViewById(R.id.toolbar_title); // get the toolbar text color, and make the drawable the same color final TypedArray a = getContext().obtainStyledAttributes(ATTRS); int toolbarTextColor = a.getColor(0, Color.WHITE); mAppsiBackgroundColor = a.getColor(1, Color.BLUE); a.recycle(); mTitleCompoundDrawable = new ExpandCollapseDrawable(getResources(), toolbarTextColor); mTitleCompoundDrawable.setExpanded(false /* expanded */, false /* animate */); int dimen = (int) (getResources().getDisplayMetrics().density * 24); mTitleCompoundDrawable.setBounds(0, 0, dimen, dimen); boolean isLtr = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_LTR; if (isLtr) { mToolbarTitle.setCompoundDrawables(null, null, mTitleCompoundDrawable, null); } else { mToolbarTitle.setCompoundDrawables(mTitleCompoundDrawable, null, null, null); } mToolbarTitle.setOnClickListener(this); mHeaderContainer = view.findViewById(R.id.header_container); mHeaderView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE) { View c0 = mHeaderView.getChildAt(0); View c1 = mHeaderView.getChildAt(1); boolean halfWay = c1 != null && c1.getTop() < c1.getHeight() / 2; boolean neededAdjustment = false; if (halfWay) { int scroll = c1.getTop(); if (scroll > 0) { mHeaderView.smoothScrollBy(0, scroll); neededAdjustment = true; } } else { int top = c0.getTop(); if (top < 0) { mHeaderView.smoothScrollBy(0, top); neededAdjustment = true; } } if (!neededAdjustment) { syncMonthToDay(mHeaderView.getChildLayoutPosition(c0)); } } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); } }); setupDecorations(); if (mPendingPermissionError) { showPermissionError(); } } @Override protected void onResume() { super.onResume(); updateToolbarTitleAndMonthPosition(); } @Override protected void onUserVisible() { super.onUserVisible(); trackPageView(AnalyticsManager.CATEGORY_AGENDA); } @Override protected void onUserInvisible() { super.onUserInvisible(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AppsiInjector.inject(this); mUseExpandableAdapter = mPreferences.getBoolean(PREF_USE_EXPANDED_ADAPTER, false); mAgendaAdapter = createAgendaAdapter(); mDatePickerController = new DatePickerControllerImpl(); int layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()); mWeekNumberDecoration = new WeekNumberDecoration(mDatePickerController, getContext(), layoutDirection); mSimpleMonthAdapter = new SimpleMonthAdapter2(getContext(), mDatePickerController); if (layoutDirection == View.LAYOUT_DIRECTION_LTR) { mSpacingDecoration = new SpacingItemDecoration(getContext(), 72, 4, 8, 4); } else { mSpacingDecoration = new SpacingItemDecoration(getContext(), 8, 4, 72, 4); } mOnClickListener = new OnClickListener(); mWeekNumberDecoration.setAgendaAdapter(mAgendaAdapter); mAgendaAdapter.setOnAgendaItemClickListener(mOnClickListener); getLoaderManager().initLoader(AGENDA_LOADER_ID, null, new AgendaEventsLoaderManager()); getLoaderManager().initLoader(AGENDA_EVENT_DAYS_LOADER_ID, null, new AgendaDaysLoaderManager()); } @Override protected void onFirstLayout() { super.onFirstLayout(); scrollToToday(); } void scrollToToday() { View view = getView(); if (view == null) return; view.post(new Runnable() { @Override public void run() { int today = Time.getJulianDay(System.currentTimeMillis(), 0); int position = mAgendaAdapter.positionOfJulianDay(today); if (position != -1) { LinearLayoutManager lm = (LinearLayoutManager) mAgendaRecycler.getLayoutManager(); lm.scrollToPositionWithOffset(position, mToolbar.getHeight()); updateToolbarTitleAndMonthPosition(); } } }); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); // if (mAgendaRecycler != null) { // mAgendaRecycler.onTrimMemory(level); // } } @Override protected void applyToolbarColor(int color) { mToolbar.setBackgroundColor(mPrimaryColor); } AgendaAdapter<?> createAgendaAdapter() { if (mUseExpandableAdapter) { return new ExpandableAgendaAdapter(getContext()); } else { return new ColoredAgendaAdapter(); } } void scrollAgendaViewToDate(int year, int month, int day, boolean fromTap) { int agendaPosition = mAgendaAdapter.positionOfDate(year, month, day); LinearLayoutManager lm = (LinearLayoutManager) mAgendaRecycler.getLayoutManager(); // When the day was tapped, toolbar is visible. In this case scroll the view // until right below the toolbar. if (fromTap) { lm.scrollToPositionWithOffset(agendaPosition, mToolbar.getHeight()); mAgendaAdapter.focusPosition(agendaPosition); } else { lm.scrollToPositionWithOffset(agendaPosition, 0); } if (fromTap) { collapseHeader(); } } void syncDayToMonth(int firstPosition) { if (firstPosition != mLastVisiblePosition) { mLastVisiblePosition = firstPosition; updateToolbarTitleAndMonthPosition(); } } private void updateTintColors() { Menu menu = mToolbar.getMenu(); MenuItem menuItemViewNormal = menu.findItem(R.id.view_type_normal); MenuItem menuItemViewCollapse = menu.findItem(R.id.view_type_collapsable); if (!mUseExpandableAdapter) { DrawableCompat.setTintColorCompat(menuItemViewNormal.getIcon(), mAccentColor); DrawableCompat.setTintColorCompat(menuItemViewCollapse.getIcon(), mDefaultTintColor); } else { DrawableCompat.setTintColorCompat(menuItemViewNormal.getIcon(), mDefaultTintColor); DrawableCompat.setTintColorCompat(menuItemViewCollapse.getIcon(), mAccentColor); } } /** * Called when the month-view was scrolled. This syncs the list to the given day. */ void syncMonthToDay(int childPosition) { mDatePickerController.moveSelectionToChild(childPosition); mSimpleMonthAdapter.setSelectedDay(mDatePickerController.mSelected); } void setupDecorations() { mAgendaRecycler.removeItemDecoration(mSpacingDecoration); mAgendaRecycler.removeItemDecoration(mWeekNumberDecoration); if (!mUseExpandableAdapter) { mAgendaRecycler.addItemDecoration(mSpacingDecoration); mAgendaRecycler.addItemDecoration(mWeekNumberDecoration); } } private void showPermissionError() { mPendingPermissionError = false; PermissionHelper permissionHelper = new PermissionHelper( R.string.permission_reason_calendar, false, this, Manifest.permission.READ_CALENDAR); permissionHelper.show(mPermissionOverlay); } void collapseHeader() { mHeaderExpanded = false; mAgendaRecycler.animate().translationY(0).setListener(mCloseListener); mTitleCompoundDrawable.setExpanded(false, true); } /** * Updates the position in */ void updateToolbarTitleAndMonthPosition() { // first acquire the current year, we need that for the method that // updates the header text mTime.setToNow(); int yearNow = mTime.year; // Next, get the time at the first visible position int firstVisiblePosition = getFirstVisiblePositionInAgendaView(); if (mAgendaAdapter.getTimeAtPosition(mTime, firstVisiblePosition)) { // Now get the position of that time in the month-picker view int positionInMonthAdapter = mDatePickerController.getPositionOfTime(mTime); RecyclerView.LayoutManager lm = mHeaderView.getLayoutManager(); // first scroll to 0 to make sure we are at the top of the view when the // month pager is opened lm.scrollToPosition(positionInMonthAdapter); // Next update the text of the toolbar mToolbarTitle.setText(getMonthAndYearString(yearNow, mTime)); } } /** * Returns the position in the adapter of the item that is shown at the top of the agenda * view. */ private int getFirstVisiblePositionInAgendaView() { View child = mAgendaRecycler.getChildAt(0); int firstVisiblePosition; if (child != null) { firstVisiblePosition = mAgendaRecycler.getChildLayoutPosition(child); } else { firstVisiblePosition = 0; } return firstVisiblePosition; } private String getMonthAndYearString(int yearNow, Time time) { mStringBuilder.setLength(0); int flags; // When in the same year, format as: January. Else format as Jan. 2014 if (time.year == yearNow) { flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY; } else { flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH; } long millis = time.toMillis(true); return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, Time.getCurrentTimezone()).toString(); } boolean onCalendarEventClicked(AgendaEvent event) { long id = event.id; long startMillis = event.startMillis; long endMillis = event.endMillis; Uri eventDetailsUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id); Intent intent = new Intent(Intent.ACTION_VIEW).setData(eventDetailsUri); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); intent.putExtra("beginTime", startMillis); intent.putExtra("endTime", endMillis); // track in google analytics track(AnalyticsManager.ACTION_OPEN_ITEM, AnalyticsManager.CATEGORY_AGENDA); getContext().startActivity(intent); return true; } boolean onEditCalendarEventClicked(AgendaEvent event) { long id = event.id; long startMillis = event.startMillis; long endMillis = event.endMillis; Uri eventDetailsUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id); Intent intent = new Intent(Intent.ACTION_EDIT).setData(eventDetailsUri); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); intent.putExtra("beginTime", startMillis); intent.putExtra("endTime", endMillis); track(AnalyticsManager.ACTION_EDIT_ITEM, AnalyticsManager.CATEGORY_AGENDA); getContext().startActivity(intent); return true; } boolean onDeleteCalendarEventClicked(AgendaEvent event) { long id = event.id; long startMillis = event.startMillis; long endMillis = event.endMillis; Uri eventDetailsUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id); Intent intent = new Intent(Intent.ACTION_DELETE).setData(eventDetailsUri); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); intent.putExtra("beginTime", startMillis); intent.putExtra("endTime", endMillis); track(AnalyticsManager.ACTION_DELETE_ITEM, AnalyticsManager.CATEGORY_AGENDA); getContext().startActivity(intent); return true; } void onAgendaDaysResult(AgendaDaysResult data) { if (data == null) { Crashlytics.logException( new NullPointerException("null data received in agenda controller")); data = new AgendaDaysResult(new SparseBooleanArray(0)); } if (data.mException != null) { onPermissionDenied(data.mException); } else { onAgendaDaysLoaded(data.mResult); if (mPermissionOverlay != null) { mPermissionOverlay.removeAllViews(); } } } private void onPermissionDenied(PermissionDeniedException permissionDeniedException) { if (mPermissionOverlay == null) { mPendingPermissionError = true; } else { showPermissionError(); } } void onAgendaDaysLoaded(SparseBooleanArray data) { mDatePickerController.setEventDays(data); mSimpleMonthAdapter.notifyDataSetChanged(); } void onAgendaEventsResult(AgendaEventsResult data) { if (data == null) { data = new AgendaEventsResult(CollectionUtils.<AgendaEvent>emptyList()); Crashlytics.logException( new NullPointerException("data == null; this should not happen")); } if (data.mPermissionDeniedException != null) { onPermissionDenied(data.mPermissionDeniedException); } else { onAgendaEventsLoaded(data.mAgendaEvents); if (mPermissionOverlay != null) { mPermissionOverlay.removeAllViews(); } } } void onAgendaEventsLoaded(List<AgendaEvent> data) { mAgendaEvents = data; int oldCount = mAgendaAdapter.getItemCount(); mAgendaAdapter.setAgendaEvents(data); if (oldCount <= 1) { scrollToToday(); } } @Override public void onClick(View v) { int id = v.getId(); if (id == R.id.toolbar_title) { // handle header click onHeaderClicked(); } } private void onHeaderClicked() { if (!mHeaderExpanded) { expandHeader(); } else { collapseHeader(); } } void expandHeader() { mHeaderExpanded = true; copyAgendaPositionToDatePickerController(); mHeaderContainer.setVisibility(View.VISIBLE); // mHeaderContainer.animate().translationY(0).setListener(null); mAgendaRecycler.setBackgroundColor(mAppsiBackgroundColor); mAgendaRecycler.animate().translationY(mHeaderWrapperHeight).setListener(null); mTitleCompoundDrawable.setExpanded(true, true); applyToolbarColor(mPrimaryColor); } /** * Updates the position in */ void copyAgendaPositionToDatePickerController() { // first acquire the current year, we need that for the method that // updates the header text mTime.setToNow(); int yearNow = mTime.year; // Next, get the time at the first visible position int firstVisiblePosition = getFirstVisiblePositionInAgendaView(); if (mAgendaAdapter.getTimeAtPosition(mTime, firstVisiblePosition)) { int positionInMonthAdapter = mDatePickerController.getPositionOfTime(mTime); // Now get the position of that time in the month-picker view mDatePickerController.onDayOfMonthSelected(mTime.year, mTime.month, mTime.monthDay, false); mSimpleMonthAdapter.notifyItemChanged(positionInMonthAdapter); } } @Override public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { if (transitionType == LayoutTransition.CHANGE_APPEARING) { mAgendaRecycler.animate().translationY(0); } } @Override public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { } @Override public boolean onMenuItemClick(MenuItem menuItem) { int itemId = menuItem.getItemId(); switch (itemId) { case R.id.action_new_calendar_event: Intent intent = new Intent(Intent.ACTION_INSERT) .setData(CalendarContract.Events.CONTENT_URI); try { getContext().startActivity(intent); } catch (ActivityNotFoundException e) { Crashlytics.logException(e); Toast.makeText(getContext(), R.string.no_compatible_calendar_app, Toast.LENGTH_SHORT).show(); } track(AnalyticsManager.ACTION_CREATE_ITEM, AnalyticsManager.CATEGORY_AGENDA); return true; case R.id.action_go_to_today: scrollToToday(); return true; case R.id.view_type_normal: if (mUseExpandableAdapter) { changeAdapterType(false); } return true; case R.id.view_type_collapsable: if (!mUseExpandableAdapter) { changeAdapterType(true); } return true; } return false; } private void changeAdapterType(boolean useExpandableAdapter) { mUseExpandableAdapter = useExpandableAdapter; mPreferences.edit().putBoolean(PREF_USE_EXPANDED_ADAPTER, useExpandableAdapter).apply(); onAdapterTypeChanged(); } void onAdapterTypeChanged() { mAgendaAdapter = createAgendaAdapter(); if (mAgendaEvents != null) { mAgendaAdapter.setAgendaEvents(mAgendaEvents); } mAgendaRecycler.setAdapter(mAgendaAdapter); mAgendaAdapter.setOnAgendaItemClickListener(mOnClickListener); updateTintColors(); setupDecorations(); } @Override public void onAccepted(PermissionHelper permissionHelper) { Intent intent = mPermissionUtils. buildRequestPermissionsIntent(getContext(), PermissionUtils.REQUEST_CODE_PERMISSION_READ_CALENDAR, Manifest.permission.READ_CALENDAR); getContext().startActivity(intent); } @Override public void onCancelled(PermissionHelper permissionHelper, boolean dontShowAgain) { // this option is not available so this method won't be called } //0416 566 090 9-11 14-16 private static class DatePickerControllerImpl implements DatePickerController { final MonthAdapter.CalendarDay mMinDate; final MonthAdapter.CalendarDay mMaxDate; final Time mTime = new Time(Time.TIMEZONE_UTC); final MonthAdapter.CalendarDay mSelected; private final List<OnDateChangedListener> mListeners = new ArrayList<>(3); private SparseBooleanArray mEventDays; DatePickerControllerImpl() { mSelected = new MonthAdapter.CalendarDay(Calendar.getInstance()); int day = TimeUtils.getJulianDay(); Time time = new Time(Time.TIMEZONE_UTC); time.setJulianDay(day); time.year -= 1; time.month = 0; time.monthDay = 1; long millis = time.normalize(true); mMinDate = new MonthAdapter.CalendarDay(millis); time.year += 3; millis = time.normalize(true); mMaxDate = new MonthAdapter.CalendarDay(millis); } @Override public boolean hasEventOnJulianDay(int julianDay) { if (mEventDays == null) return false; return mEventDays.get(julianDay); } @Override public void onYearSelected(int year) { mSelected.setDay(year, mSelected.month, mSelected.day); } @Override public void onDayOfMonthSelected(int year, int month, int day, boolean fromTap) { mSelected.setDay(year, month, day); notifySelectionChanged(fromTap); } @Override public void registerOnDateChangedListener(OnDateChangedListener listener) { mListeners.add(listener); } @Override public void unregisterOnDateChangedListener(OnDateChangedListener listener) { mListeners.remove(listener); } @Override public MonthAdapter.CalendarDay getSelectedDay() { return mSelected; } @Override public int getFirstDayOfWeek() { return Calendar.MONDAY; } @Override public int getMinYear() { return 2014; } @Override public int getMaxYear() { return 2020; } @Override public Calendar getMinDate() { Calendar c = Calendar.getInstance(); c.set(2014, Calendar.JANUARY, 1); return c; } @Override public Calendar getMaxDate() { Calendar c = Calendar.getInstance(); c.set(2020, Calendar.DECEMBER, 31); return c; } @Override public void tryVibrate() { } MonthAdapter.CalendarDay ensureMaxDate() { return mMaxDate; } public int getPositionOfTime(Time time) { MonthAdapter.CalendarDay minDate = mMinDate; int yearsDelta = time.year - minDate.year; int monthsDelta = time.month - minDate.month; return monthsDelta + yearsDelta * 12; } public void moveSelectionToChild(int childPosition) { MonthAdapter.CalendarDay minDate = mMinDate; mTime.set(minDate.day, minDate.month + childPosition, minDate.year); mTime.normalize(true); int max = mTime.getActualMaximum(Time.MONTH_DAY); int day = Math.min(mSelected.day, max); setSelection(mTime.year, mTime.month, day); } void setSelection(int year, int month, int monthDay) { onDayOfMonthSelected(year, month, monthDay, false); } private void notifySelectionChanged(boolean fromTap) { int N = mListeners.size(); for (int i = 0; i < N; i++) { OnDateChangedListener l = mListeners.get(i); l.onDateChanged(mSelected.year, mSelected.month, mSelected.day, fromTap); } } public void setEventDays(SparseBooleanArray eventDays) { mEventDays = eventDays; } } static class WeekNumberDecoration extends RecyclerView.ItemDecoration { private static final int[] ATTRS = new int[]{ R.attr.colorControlNormal, R.attr.colorAccent, R.attr.colorPrimary }; final DatePickerControllerImpl mController; final int mFirstDayOfWeek; final int mTopSpacing; final int mDaySpacing; final int mWeekNumberOffset; final int mLeftOffset; final Paint mWeekNumberPaint; final Paint mDayNamePaint; final Paint mDayNumberPaint; final Paint mDayNamePaintToday; final Paint mDayNumberPaintToday; final int mDayNumberOffset; final int mDayNameOffset; final Rect mMeasureTextRect = new Rect(); final Time mTime = new Time(Time.TIMEZONE_UTC); final Calendar mCalendarInstance = Calendar.getInstance(TimeZone.getTimeZone(Time.TIMEZONE_UTC), Locale.getDefault()); final Context mContext; final Formatter mFormatter; final StringBuilder mStringBuilder; final int mLayoutDirection; final NumberFormat mNumberFormat = NumberFormat.getIntegerInstance(); AgendaAdapter<?> mAgendaAdapter; WeekNumberDecoration(DatePickerControllerImpl controller, Context context, int layoutDirection) { mContext = context; mController = controller; mLayoutDirection = layoutDirection; // decrement by 1 to convert calendar days to time days mFirstDayOfWeek = controller.getFirstDayOfWeek() - 1; float density = context.getResources().getDisplayMetrics().density; mTopSpacing = (int) (density * 16); mLeftOffset = (int) (density * 72); mDaySpacing = (int) (density * 20); mWeekNumberOffset = (int) (density * 20); mDayNumberOffset = (int) (density * 20); mDayNameOffset = (int) (density * 36); final TypedArray a = context.obtainStyledAttributes(ATTRS); int textColor = a.getColor(0, Color.BLACK); int accentColor = a.getColor(1, Color.BLACK); int primaryColor = a.getColor(2, Color.BLACK); a.recycle(); mStringBuilder = new StringBuilder(); mFormatter = new Formatter(mStringBuilder); mWeekNumberPaint = new Paint(); mWeekNumberPaint.setColor(textColor); mWeekNumberPaint.setTextSize(12 * density); mWeekNumberPaint.setAntiAlias(true); mDayNamePaint = new Paint(); mDayNamePaint.setColor(textColor); mDayNamePaint.setTextSize(14 * density); mDayNamePaint.setAntiAlias(true); mDayNumberPaint = new Paint(); mDayNumberPaint.setColor(textColor); mDayNumberPaint.setTextSize(28 * density); mDayNumberPaint.setAntiAlias(true); mDayNamePaintToday = new Paint(); mDayNamePaintToday.setColor(accentColor); mDayNamePaintToday.setTextSize(14 * density); mDayNamePaintToday.setAntiAlias(true); mDayNumberPaintToday = new Paint(); mDayNumberPaintToday.setColor(accentColor); mDayNumberPaintToday.setTextSize(28 * density); mDayNumberPaintToday.setAntiAlias(true); } public void setAgendaAdapter(AgendaAdapter<?> agendaAdapter) { mAgendaAdapter = agendaAdapter; } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { int childCount = parent.getChildCount(); int today = TimeUtils.getJulianDay(); int parentWidth = parent.getWidth(); for (int i = 0; i < childCount; i++) { View view = parent.getChildAt(i); int position = parent.getChildLayoutPosition(view); int top = view.getTop(); if (mAgendaAdapter != null && mAgendaAdapter.hasWeekNumberAtPosition(position)) { int startDay = mAgendaAdapter.getJulianDayAtPosition(position); long startMillis = mTime.setJulianDay(startDay); int daysAfterFirstDayOfWeek = mTime.weekDay - mFirstDayOfWeek; if (daysAfterFirstDayOfWeek < 0) daysAfterFirstDayOfWeek += 7; startMillis -= daysAfterFirstDayOfWeek * DateUtils.DAY_IN_MILLIS; long endMillis = startMillis + DateUtils.DAY_IN_MILLIS * 7; mStringBuilder.setLength(0); DateUtils.formatDateRange(mContext, mFormatter, startMillis, endMillis, DateUtils.FORMAT_SHOW_DATE, Time.TIMEZONE_UTC); String weekText = mStringBuilder.toString(); int offset; if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) { offset = mLeftOffset; } else { mWeekNumberPaint.getTextBounds( weekText, 0, weekText.length(), mMeasureTextRect); offset = parentWidth - mMeasureTextRect.width() - mLeftOffset; } c.drawText(weekText, offset, top - mWeekNumberOffset, mWeekNumberPaint); } if (needsDayNumber(position)) { int day = mAgendaAdapter.getJulianDayAtPosition(position); drawDayNumber(c, position, top, day == today, parentWidth); } } } boolean needsDayNumber(int position) { if (position == 0) return false; int julianDay = mAgendaAdapter.getJulianDayAtPosition(position); int prevPositionDay = mAgendaAdapter.getJulianDayAtPosition(position - 1); return prevPositionDay != julianDay; } private void drawDayNumber(Canvas c, int position, int top, boolean today, int parentWidth) { int julianDay = mAgendaAdapter.getJulianDayAtPosition(position); long millis = mTime.setJulianDay(julianDay); mCalendarInstance.setTimeInMillis(millis); String displayDayOfWeek = mCalendarInstance.getDisplayName( Calendar.DAY_OF_WEEK, Calendar.SHORT, Locale.getDefault()); int monthDay = mTime.monthDay; final int dayNameOffset; final int dayNumberOffset; String dayNumber = mNumberFormat.format(monthDay); if (mLayoutDirection == View.LAYOUT_DIRECTION_LTR) { dayNameOffset = mWeekNumberOffset; dayNumberOffset = mDayNumberOffset; } else { mDayNamePaint.getTextBounds( displayDayOfWeek, 0, displayDayOfWeek.length(), mMeasureTextRect); dayNameOffset = parentWidth - mMeasureTextRect.width() - mWeekNumberOffset; mDayNumberPaint.getTextBounds( dayNumber, 0, dayNumber.length(), mMeasureTextRect); dayNumberOffset = parentWidth - mMeasureTextRect.width() - mWeekNumberOffset; } c.drawText(displayDayOfWeek, dayNameOffset, top + mDayNameOffset, today ? mDayNamePaintToday : mDayNamePaint); c.drawText(dayNumber, dayNumberOffset, top + mDayNumberOffset, today ? mDayNumberPaintToday : mDayNumberPaint); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { int position = parent.getChildLayoutPosition(view); // see if this is a new day in the adapter. If so, add extra padding int extraOffsetForNewDay; if (position != 0) { int julianDay = mAgendaAdapter.getJulianDayAtPosition(position); int prevPositionDay = mAgendaAdapter.getJulianDayAtPosition(position - 1); boolean differentDay = prevPositionDay != julianDay; extraOffsetForNewDay = differentDay ? mDaySpacing : 0; } else { extraOffsetForNewDay = 0; } if (mAgendaAdapter != null && mAgendaAdapter.hasWeekNumberAtPosition(position)) { outRect.set(0, mTopSpacing + extraOffsetForNewDay, 0, 0); } else { outRect.set(0, extraOffsetForNewDay, 0, 0); } } } class AgendaEventsLoaderManager implements LoaderManager.LoaderCallbacks<AgendaEventsResult> { @Override public Loader<AgendaEventsResult> onCreateLoader(int id, Bundle args) { return new AgendaLoader(getContext(), mDatePickerController); } @Override public void onLoadFinished(Loader<AgendaEventsResult> loader, AgendaEventsResult data) { onAgendaEventsResult(data); } @Override public void onLoaderReset(Loader<AgendaEventsResult> loader) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. mAgendaAdapter.clear(); } } class AgendaDaysLoaderManager implements LoaderManager.LoaderCallbacks<AgendaDaysResult> { @Override public Loader<AgendaDaysResult> onCreateLoader(int id, Bundle args) { return new AgendaDaysLoader(getContext(), mDatePickerController); } @Override public void onLoadFinished(Loader<AgendaDaysResult> loader, AgendaDaysResult data) { onAgendaDaysResult(data); } @Override public void onLoaderReset(Loader<AgendaDaysResult> loader) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. } } class OnClickListener implements ItemAgendaViewHolder.OnItemClickListener { @Override public void onItemClicked(AgendaEvent event) { onCalendarEventClicked(event); } @Override public void onItemEditClicked(AgendaEvent event) { onEditCalendarEventClicked(event); } @Override public void onItemDeleteClicked(AgendaEvent event) { onDeleteCalendarEventClicked(event); } } class AgendaOnItemTouchListener implements RecyclerView.OnItemTouchListener { float mInitialTouchX; float mInitialTouchY; float mLastSetValue; @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { mInitialTouchX = e.getRawX(); mInitialTouchY = e.getRawY(); mLastSetValue = 0; return mHeaderExpanded; } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { if (mHeaderExpanded) { int action = e.getAction(); if (action == MotionEvent.ACTION_MOVE) { int deltaY = (int) (mInitialTouchY - e.getRawY()); if (deltaY <= 0) { mAgendaRecycler.setTranslationY(mHeaderWrapperHeight); mLastSetValue = 0; } else if (deltaY > mHeaderWrapperHeight) { mAgendaRecycler.setTranslationY(0); mLastSetValue = deltaY; } else { mAgendaRecycler.setTranslationY(mHeaderWrapperHeight - deltaY); mLastSetValue = deltaY; } } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { int h = mHeaderWrapperHeight; if (mLastSetValue < h / 4) { expandHeader(); mToolbar.animate().translationY(0); } else { collapseHeader(); // hideToolbar(); } mLastSetValue = 0; } } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } } }