diff options
83 files changed, 17697 insertions, 17935 deletions
diff --git a/Android.bp b/Android.bp new file mode 100644 index 00000000..3c6477b3 --- /dev/null +++ b/Android.bp @@ -0,0 +1,56 @@ +package { + + default_applicable_licenses: ["packages_apps_Calendar_license"], +} + +license { + + name: "packages_apps_Calendar_license", + visibility: [":__subpackages__"], + license_kinds: [ + "SPDX-license-identifier-Apache-2.0", + ], + license_text: [ + "NOTICE", + ], +} + +// Include res dir from chips + +android_app { + name: "Calendar", + + jacoco: { + include_filter: ["com.android.calendar.**"], + }, + + srcs: [ + "src/**/*.kt", + ], + + // bundled + //LOCAL_STATIC_JAVA_LIBRARIES += + //# android-common + //# libchips + //# calendar-common + + // unbundled + static_libs: [ + "android-common", + "libchips", + "colorpicker", + "android-opt-timezonepicker", + "androidx.legacy_legacy-support-v4", + "calendar-common", + ], + + sdk_version: "current", + target_sdk_version: "30", + optimize: { + proguard_flags_files: ["proguard.flags"], + }, + + product_specific: true, + + aaptflags: ["--auto-add-overlay"], +} diff --git a/Android.mk b/Android.mk deleted file mode 100644 index dce26a46..00000000 --- a/Android.mk +++ /dev/null @@ -1,53 +0,0 @@ -LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -# Include res dir from chips -chips_dir := ../../../frameworks/opt/chips/res -color_picker_dir := ../../../frameworks/opt/colorpicker/res -timezonepicker_dir := ../../../frameworks/opt/timezonepicker/res -res_dirs := $(chips_dir) $(color_picker_dir) $(timezonepicker_dir) res -src_dirs := src - -LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.calendar.* - -LOCAL_MODULE_TAGS := optional - -LOCAL_SRC_FILES := $(call all-java-files-under,$(src_dirs)) - -# bundled -#LOCAL_STATIC_JAVA_LIBRARIES += \ -# android-common \ -# libchips \ -# calendar-common - -# unbundled -LOCAL_STATIC_JAVA_LIBRARIES := \ - android-common \ - libchips \ - colorpicker \ - android-opt-timezonepicker \ - androidx.legacy_legacy-support-v4 \ - calendar-common - -LOCAL_SDK_VERSION := current - -LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) - -LOCAL_PACKAGE_NAME := Calendar -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/NOTICE - -LOCAL_PROGUARD_FLAG_FILES := proguard.flags - -LOCAL_PRODUCT_MODULE := true - -LOCAL_AAPT_FLAGS := --auto-add-overlay -LOCAL_AAPT_FLAGS += --extra-packages com.android.ex.chips -LOCAL_AAPT_FLAGS += --extra-packages com.android.colorpicker -LOCAL_AAPT_FLAGS += --extra-packages com.android.timezonepicker - -include $(BUILD_PACKAGE) - -# Use the following include to make our test apk. -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c9c5a04b..fed61b0c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -38,7 +38,8 @@ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.mail" /> - <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="29"></uses-sdk> + <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="30"></uses-sdk> <application android:name="CalendarApplication" diff --git a/src/com/android/calendar/AllInOneActivity.java b/src/com/android/calendar/AllInOneActivity.java deleted file mode 100644 index cec6a40f..00000000 --- a/src/com/android/calendar/AllInOneActivity.java +++ /dev/null @@ -1,1062 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar; - -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.ObjectAnimator; -import android.app.ActionBar; -import android.app.ActionBar.Tab; -import android.app.Activity; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.content.AsyncQueryHandler; -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.database.Cursor; -import android.graphics.drawable.LayerDrawable; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.provider.CalendarContract; -import android.provider.CalendarContract.Attendees; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Events; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.RelativeLayout.LayoutParams; -import android.widget.TextView; - -import com.android.calendar.CalendarController.EventHandler; -import com.android.calendar.CalendarController.EventInfo; -import com.android.calendar.CalendarController.EventType; -import com.android.calendar.CalendarController.ViewType; -import com.android.calendar.month.MonthByWeekFragment; - -import java.io.IOException; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -import static android.provider.CalendarContract.Attendees.ATTENDEE_STATUS; -import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY; -import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; -import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; - -public class AllInOneActivity extends Activity implements EventHandler, - OnSharedPreferenceChangeListener, ActionBar.TabListener, - ActionBar.OnNavigationListener { - private static final String TAG = "AllInOneActivity"; - private static final boolean DEBUG = false; - private static final String EVENT_INFO_FRAGMENT_TAG = "EventInfoFragment"; - private static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time"; - private static final String BUNDLE_KEY_EVENT_ID = "key_event_id"; - private static final String BUNDLE_KEY_RESTORE_VIEW = "key_restore_view"; - private static final String BUNDLE_KEY_CHECK_ACCOUNTS = "key_check_for_accounts"; - private static final int HANDLER_KEY = 0; - - // Indices of buttons for the drop down menu (tabs replacement) - // Must match the strings in the array buttons_list in arrays.xml and the - // OnNavigationListener - private static final int BUTTON_DAY_INDEX = 0; - private static final int BUTTON_WEEK_INDEX = 1; - private static final int BUTTON_MONTH_INDEX = 2; - private static final int BUTTON_AGENDA_INDEX = 3; - - private CalendarController mController; - private static boolean mIsMultipane; - private static boolean mIsTabletConfig; - private boolean mOnSaveInstanceStateCalled = false; - private boolean mBackToPreviousView = false; - private ContentResolver mContentResolver; - private int mPreviousView; - private int mCurrentView; - private boolean mPaused = true; - private boolean mUpdateOnResume = false; - private boolean mHideControls = false; - private boolean mShowSideViews = true; - private boolean mShowWeekNum = false; - private TextView mHomeTime; - private TextView mDateRange; - private TextView mWeekTextView; - private View mMiniMonth; - private View mCalendarsList; - private View mMiniMonthContainer; - private View mSecondaryPane; - private String mTimeZone; - private boolean mShowCalendarControls; - private boolean mShowEventInfoFullScreen; - private int mWeekNum; - private int mCalendarControlsAnimationTime; - private int mControlsAnimateWidth; - private int mControlsAnimateHeight; - - private long mViewEventId = -1; - private long mIntentEventStartMillis = -1; - private long mIntentEventEndMillis = -1; - private int mIntentAttendeeResponse = Attendees.ATTENDEE_STATUS_NONE; - private boolean mIntentAllDay = false; - - // Action bar and Navigation bar (left side of Action bar) - private ActionBar mActionBar; - private ActionBar.Tab mDayTab; - private ActionBar.Tab mWeekTab; - private ActionBar.Tab mMonthTab; - private MenuItem mControlsMenu; - private Menu mOptionsMenu; - private CalendarViewAdapter mActionBarMenuSpinnerAdapter; - private QueryHandler mHandler; - private boolean mCheckForAccounts = true; - - private String mHideString; - private String mShowString; - - DayOfMonthDrawable mDayOfMonthIcon; - - int mOrientation; - - // Params for animating the controls on the right - private LayoutParams mControlsParams; - private LinearLayout.LayoutParams mVerticalControlsParams; - - private final AnimatorListener mSlideAnimationDoneListener = new AnimatorListener() { - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationEnd(android.animation.Animator animation) { - int visibility = mShowSideViews ? View.VISIBLE : View.GONE; - mMiniMonth.setVisibility(visibility); - mCalendarsList.setVisibility(visibility); - mMiniMonthContainer.setVisibility(visibility); - } - - @Override - public void onAnimationRepeat(android.animation.Animator animation) { - } - - @Override - public void onAnimationStart(android.animation.Animator animation) { - } - }; - - private class QueryHandler extends AsyncQueryHandler { - public QueryHandler(ContentResolver cr) { - super(cr); - } - - @Override - protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - mCheckForAccounts = false; - try { - // If the query didn't return a cursor for some reason return - if (cursor == null || cursor.getCount() > 0 || isFinishing()) { - return; - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - Bundle options = new Bundle(); - options.putCharSequence("introMessage", - getResources().getString(R.string.create_an_account_desc)); - options.putBoolean("allowSkip", true); - - AccountManager am = AccountManager.get(AllInOneActivity.this); - am.addAccount("com.google", CalendarContract.AUTHORITY, null, options, - AllInOneActivity.this, - new AccountManagerCallback<Bundle>() { - @Override - public void run(AccountManagerFuture<Bundle> future) { - } - }, null); - } - } - - private final Runnable mHomeTimeUpdater = new Runnable() { - @Override - public void run() { - mTimeZone = Utils.getTimeZone(AllInOneActivity.this, mHomeTimeUpdater); - updateSecondaryTitleFields(-1); - AllInOneActivity.this.invalidateOptionsMenu(); - Utils.setMidnightUpdater(mHandler, mTimeChangesUpdater, mTimeZone); - } - }; - - // runs every midnight/time changes and refreshes the today icon - private final Runnable mTimeChangesUpdater = new Runnable() { - @Override - public void run() { - mTimeZone = Utils.getTimeZone(AllInOneActivity.this, mHomeTimeUpdater); - AllInOneActivity.this.invalidateOptionsMenu(); - Utils.setMidnightUpdater(mHandler, mTimeChangesUpdater, mTimeZone); - } - }; - - - // Create an observer so that we can update the views whenever a - // Calendar event changes. - private final ContentObserver mObserver = new ContentObserver(new Handler()) { - @Override - public boolean deliverSelfNotifications() { - return true; - } - - @Override - public void onChange(boolean selfChange) { - eventsChanged(); - } - }; - - @Override - protected void onNewIntent(Intent intent) { - String action = intent.getAction(); - if (DEBUG) - Log.d(TAG, "New intent received " + intent.toString()); - // Don't change the date if we're just returning to the app's home - if (Intent.ACTION_VIEW.equals(action) - && !intent.getBooleanExtra(Utils.INTENT_KEY_HOME, false)) { - long millis = parseViewAction(intent); - if (millis == -1) { - millis = Utils.timeFromIntentInMillis(intent); - } - if (millis != -1 && mViewEventId == -1 && mController != null) { - Time time = new Time(mTimeZone); - time.set(millis); - time.normalize(true); - mController.sendEvent(this, EventType.GO_TO, time, time, -1, ViewType.CURRENT); - } - } - } - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - if (icicle != null && icicle.containsKey(BUNDLE_KEY_CHECK_ACCOUNTS)) { - mCheckForAccounts = icicle.getBoolean(BUNDLE_KEY_CHECK_ACCOUNTS); - } - // Launch add google account if this is first time and there are no - // accounts yet - if (mCheckForAccounts) { - - mHandler = new QueryHandler(this.getContentResolver()); - mHandler.startQuery(0, null, Calendars.CONTENT_URI, new String[] { - Calendars._ID - }, null, null /* selection args */, null /* sort order */); - } - - // This needs to be created before setContentView - mController = CalendarController.getInstance(this); - - - // Get time from intent or icicle - long timeMillis = -1; - int viewType = -1; - final Intent intent = getIntent(); - if (icicle != null) { - timeMillis = icicle.getLong(BUNDLE_KEY_RESTORE_TIME); - viewType = icicle.getInt(BUNDLE_KEY_RESTORE_VIEW, -1); - } else { - String action = intent.getAction(); - if (Intent.ACTION_VIEW.equals(action)) { - // Open EventInfo later - timeMillis = parseViewAction(intent); - } - - if (timeMillis == -1) { - timeMillis = Utils.timeFromIntentInMillis(intent); - } - } - - if (viewType == -1 || viewType > ViewType.MAX_VALUE) { - viewType = Utils.getViewTypeFromIntentAndSharedPref(this); - } - mTimeZone = Utils.getTimeZone(this, mHomeTimeUpdater); - Time t = new Time(mTimeZone); - t.set(timeMillis); - - if (DEBUG) { - if (icicle != null && intent != null) { - Log.d(TAG, "both, icicle:" + icicle.toString() + " intent:" + intent.toString()); - } else { - Log.d(TAG, "not both, icicle:" + icicle + " intent:" + intent); - } - } - - Resources res = getResources(); - mHideString = res.getString(R.string.hide_controls); - mShowString = res.getString(R.string.show_controls); - mOrientation = res.getConfiguration().orientation; - if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) { - mControlsAnimateWidth = (int)res.getDimension(R.dimen.calendar_controls_width); - if (mControlsParams == null) { - mControlsParams = new LayoutParams(mControlsAnimateWidth, 0); - } - mControlsParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); - } else { - // Make sure width is in between allowed min and max width values - mControlsAnimateWidth = Math.max(res.getDisplayMetrics().widthPixels * 45 / 100, - (int)res.getDimension(R.dimen.min_portrait_calendar_controls_width)); - mControlsAnimateWidth = Math.min(mControlsAnimateWidth, - (int)res.getDimension(R.dimen.max_portrait_calendar_controls_width)); - } - - mControlsAnimateHeight = (int)res.getDimension(R.dimen.calendar_controls_height); - - mHideControls = true; - mIsMultipane = Utils.getConfigBool(this, R.bool.multiple_pane_config); - mIsTabletConfig = Utils.getConfigBool(this, R.bool.tablet_config); - mShowCalendarControls = - Utils.getConfigBool(this, R.bool.show_calendar_controls); - mShowEventInfoFullScreen = - Utils.getConfigBool(this, R.bool.show_event_info_full_screen); - mCalendarControlsAnimationTime = res.getInteger(R.integer.calendar_controls_animation_time); - Utils.setAllowWeekForDetailView(mIsMultipane); - - // setContentView must be called before configureActionBar - setContentView(R.layout.all_in_one); - - if (mIsTabletConfig) { - mDateRange = (TextView) findViewById(R.id.date_bar); - mWeekTextView = (TextView) findViewById(R.id.week_num); - } else { - mDateRange = (TextView) getLayoutInflater().inflate(R.layout.date_range_title, null); - } - - // configureActionBar auto-selects the first tab you add, so we need to - // call it before we set up our own fragments to make sure it doesn't - // overwrite us - configureActionBar(viewType); - - mHomeTime = (TextView) findViewById(R.id.home_time); - mMiniMonth = findViewById(R.id.mini_month); - if (mIsTabletConfig && mOrientation == Configuration.ORIENTATION_PORTRAIT) { - mMiniMonth.setLayoutParams(new RelativeLayout.LayoutParams(mControlsAnimateWidth, - mControlsAnimateHeight)); - } - mCalendarsList = findViewById(R.id.calendar_list); - mMiniMonthContainer = findViewById(R.id.mini_month_container); - mSecondaryPane = findViewById(R.id.secondary_pane); - - // Must register as the first activity because this activity can modify - // the list of event handlers in it's handle method. This affects who - // the rest of the handlers the controller dispatches to are. - mController.registerFirstEventHandler(HANDLER_KEY, this); - - initFragments(timeMillis, viewType, icicle); - - // Listen for changes that would require this to be refreshed - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(this); - prefs.registerOnSharedPreferenceChangeListener(this); - - mContentResolver = getContentResolver(); - } - - private long parseViewAction(final Intent intent) { - long timeMillis = -1; - Uri data = intent.getData(); - if (data != null && data.isHierarchical()) { - List<String> path = data.getPathSegments(); - if (path.size() == 2 && path.get(0).equals("events")) { - try { - mViewEventId = Long.valueOf(data.getLastPathSegment()); - if (mViewEventId != -1) { - mIntentEventStartMillis = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, 0); - mIntentEventEndMillis = intent.getLongExtra(EXTRA_EVENT_END_TIME, 0); - mIntentAttendeeResponse = intent.getIntExtra( - ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE); - mIntentAllDay = intent.getBooleanExtra(EXTRA_EVENT_ALL_DAY, false); - timeMillis = mIntentEventStartMillis; - } - } catch (NumberFormatException e) { - // Ignore if mViewEventId can't be parsed - } - } - } - return timeMillis; - } - - private void configureActionBar(int viewType) { - createButtonsSpinner(viewType, mIsTabletConfig); - if (mIsMultipane) { - mActionBar.setDisplayOptions( - ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME); - } else { - mActionBar.setDisplayOptions(0); - } - } - - private void createButtonsSpinner(int viewType, boolean tabletConfig) { - // If tablet configuration , show spinner with no dates - mActionBarMenuSpinnerAdapter = new CalendarViewAdapter (this, viewType, !tabletConfig); - mActionBar = getActionBar(); - mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); - mActionBar.setListNavigationCallbacks(mActionBarMenuSpinnerAdapter, this); - switch (viewType) { - case ViewType.AGENDA: - break; - case ViewType.DAY: - mActionBar.setSelectedNavigationItem(BUTTON_DAY_INDEX); - break; - case ViewType.WEEK: - mActionBar.setSelectedNavigationItem(BUTTON_WEEK_INDEX); - break; - case ViewType.MONTH: - mActionBar.setSelectedNavigationItem(BUTTON_MONTH_INDEX); - break; - default: - mActionBar.setSelectedNavigationItem(BUTTON_DAY_INDEX); - break; - } - } - // Clear buttons used in the agenda view - private void clearOptionsMenu() { - if (mOptionsMenu == null) { - return; - } - MenuItem cancelItem = mOptionsMenu.findItem(R.id.action_cancel); - if (cancelItem != null) { - cancelItem.setVisible(false); - } - } - - @Override - protected void onResume() { - super.onResume(); - - // Check if the upgrade code has ever been run. If not, force a sync just this one time. - Utils.trySyncAndDisableUpgradeReceiver(this); - - // Must register as the first activity because this activity can modify - // the list of event handlers in it's handle method. This affects who - // the rest of the handlers the controller dispatches to are. - mController.registerFirstEventHandler(HANDLER_KEY, this); - - mOnSaveInstanceStateCalled = false; - mContentResolver.registerContentObserver(CalendarContract.Events.CONTENT_URI, - true, mObserver); - if (mUpdateOnResume) { - initFragments(mController.getTime(), mController.getViewType(), null); - mUpdateOnResume = false; - } - Time t = new Time(mTimeZone); - t.set(mController.getTime()); - mController.sendEvent(this, EventType.UPDATE_TITLE, t, t, -1, ViewType.CURRENT, - mController.getDateFlags(), null, null); - // Make sure the drop-down menu will get its date updated at midnight - if (mActionBarMenuSpinnerAdapter != null) { - mActionBarMenuSpinnerAdapter.refresh(this); - } - - if (mControlsMenu != null) { - mControlsMenu.setTitle(mHideControls ? mShowString : mHideString); - } - mPaused = false; - - if (mViewEventId != -1 && mIntentEventStartMillis != -1 && mIntentEventEndMillis != -1) { - long currentMillis = System.currentTimeMillis(); - long selectedTime = -1; - if (currentMillis > mIntentEventStartMillis && currentMillis < mIntentEventEndMillis) { - selectedTime = currentMillis; - } - mController.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, mViewEventId, - mIntentEventStartMillis, mIntentEventEndMillis, -1, -1, - EventInfo.buildViewExtraLong(mIntentAttendeeResponse,mIntentAllDay), - selectedTime); - mViewEventId = -1; - mIntentEventStartMillis = -1; - mIntentEventEndMillis = -1; - mIntentAllDay = false; - } - Utils.setMidnightUpdater(mHandler, mTimeChangesUpdater, mTimeZone); - // Make sure the today icon is up to date - invalidateOptionsMenu(); - } - - @Override - protected void onPause() { - super.onPause(); - - mController.deregisterEventHandler(HANDLER_KEY); - mPaused = true; - mHomeTime.removeCallbacks(mHomeTimeUpdater); - if (mActionBarMenuSpinnerAdapter != null) { - mActionBarMenuSpinnerAdapter.onPause(); - } - mContentResolver.unregisterContentObserver(mObserver); - if (isFinishing()) { - // Stop listening for changes that would require this to be refreshed - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(this); - prefs.unregisterOnSharedPreferenceChangeListener(this); - } - // FRAG_TODO save highlighted days of the week; - if (mController.getViewType() != ViewType.EDIT) { - Utils.setDefaultView(this, mController.getViewType()); - } - Utils.resetMidnightUpdater(mHandler, mTimeChangesUpdater); - } - - @Override - protected void onUserLeaveHint() { - mController.sendEvent(this, EventType.USER_HOME, null, null, -1, ViewType.CURRENT); - super.onUserLeaveHint(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - mOnSaveInstanceStateCalled = true; - super.onSaveInstanceState(outState); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(this); - prefs.unregisterOnSharedPreferenceChangeListener(this); - - mController.deregisterAllEventHandlers(); - - CalendarController.removeInstance(this); - } - - private void initFragments(long timeMillis, int viewType, Bundle icicle) { - if (DEBUG) { - Log.d(TAG, "Initializing to " + timeMillis + " for view " + viewType); - } - FragmentTransaction ft = getFragmentManager().beginTransaction(); - - if (mShowCalendarControls) { - Fragment miniMonthFrag = new MonthByWeekFragment(timeMillis, true); - ft.replace(R.id.mini_month, miniMonthFrag); - mController.registerEventHandler(R.id.mini_month, (EventHandler) miniMonthFrag); - } - if (!mShowCalendarControls || viewType == ViewType.EDIT) { - mMiniMonth.setVisibility(View.GONE); - mCalendarsList.setVisibility(View.GONE); - } - - EventInfo info = null; - if (viewType == ViewType.EDIT) { - mPreviousView = GeneralPreferences.getSharedPreferences(this).getInt( - GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW); - - long eventId = -1; - Intent intent = getIntent(); - Uri data = intent.getData(); - if (data != null) { - try { - eventId = Long.parseLong(data.getLastPathSegment()); - } catch (NumberFormatException e) { - if (DEBUG) { - Log.d(TAG, "Create new event"); - } - } - } else if (icicle != null && icicle.containsKey(BUNDLE_KEY_EVENT_ID)) { - eventId = icicle.getLong(BUNDLE_KEY_EVENT_ID); - } - - long begin = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, -1); - long end = intent.getLongExtra(EXTRA_EVENT_END_TIME, -1); - info = new EventInfo(); - if (end != -1) { - info.endTime = new Time(); - info.endTime.set(end); - } - if (begin != -1) { - info.startTime = new Time(); - info.startTime.set(begin); - } - info.id = eventId; - // We set the viewtype so if the user presses back when they are - // done editing the controller knows we were in the Edit Event - // screen. Likewise for eventId - mController.setViewType(viewType); - mController.setEventId(eventId); - } else { - mPreviousView = viewType; - } - - setMainPane(ft, R.id.main_pane, viewType, timeMillis, true); - ft.commit(); // this needs to be after setMainPane() - - Time t = new Time(mTimeZone); - t.set(timeMillis); - if (viewType != ViewType.EDIT) { - mController.sendEvent(this, EventType.GO_TO, t, null, -1, viewType); - } - } - - @Override - public void onBackPressed() { - if (mCurrentView == ViewType.EDIT || mBackToPreviousView) { - mController.sendEvent(this, EventType.GO_TO, null, null, -1, mPreviousView); - } else { - super.onBackPressed(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - mOptionsMenu = menu; - getMenuInflater().inflate(R.menu.all_in_one_title_bar, menu); - - // Hide the "show/hide controls" button if this is a phone - // or the view type is "Month". - - mControlsMenu = menu.findItem(R.id.action_hide_controls); - if (!mShowCalendarControls) { - if (mControlsMenu != null) { - mControlsMenu.setVisible(false); - mControlsMenu.setEnabled(false); - } - } else if (mControlsMenu != null && mController != null - && (mController.getViewType() == ViewType.MONTH)) { - mControlsMenu.setVisible(false); - mControlsMenu.setEnabled(false); - } else if (mControlsMenu != null){ - mControlsMenu.setTitle(mHideControls ? mShowString : mHideString); - } - - MenuItem menuItem = menu.findItem(R.id.action_today); - if (Utils.isJellybeanOrLater()) { - // replace the default top layer drawable of the today icon with a - // custom drawable that shows the day of the month of today - LayerDrawable icon = (LayerDrawable) menuItem.getIcon(); - Utils.setTodayIcon(icon, this, mTimeZone); - } else { - menuItem.setIcon(R.drawable.ic_menu_today_no_date_holo_light); - } - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - Time t = null; - int viewType = ViewType.CURRENT; - long extras = CalendarController.EXTRA_GOTO_TIME; - final int itemId = item.getItemId(); - if (itemId == R.id.action_today) { - viewType = ViewType.CURRENT; - t = new Time(mTimeZone); - t.setToNow(); - extras |= CalendarController.EXTRA_GOTO_TODAY; - } else if (itemId == R.id.action_hide_controls) { - mHideControls = !mHideControls; - item.setTitle(mHideControls ? mShowString : mHideString); - if (!mHideControls) { - mMiniMonth.setVisibility(View.VISIBLE); - mCalendarsList.setVisibility(View.VISIBLE); - mMiniMonthContainer.setVisibility(View.VISIBLE); - } - final ObjectAnimator slideAnimation = ObjectAnimator.ofInt(this, "controlsOffset", - mHideControls ? 0 : mControlsAnimateWidth, - mHideControls ? mControlsAnimateWidth : 0); - slideAnimation.setDuration(mCalendarControlsAnimationTime); - ObjectAnimator.setFrameDelay(0); - slideAnimation.start(); - return true; - } else { - Log.d(TAG, "Unsupported itemId: " + itemId); - return true; - } - mController.sendEvent(this, EventType.GO_TO, t, null, t, -1, viewType, extras, null, null); - return true; - } - - /** - * Sets the offset of the controls on the right for animating them off/on - * screen. ProGuard strips this if it's not in proguard.flags - * - * @param controlsOffset The current offset in pixels - */ - public void setControlsOffset(int controlsOffset) { - if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) { - mMiniMonth.setTranslationX(controlsOffset); - mCalendarsList.setTranslationX(controlsOffset); - mControlsParams.width = Math.max(0, mControlsAnimateWidth - controlsOffset); - mMiniMonthContainer.setLayoutParams(mControlsParams); - } else { - mMiniMonth.setTranslationY(controlsOffset); - mCalendarsList.setTranslationY(controlsOffset); - if (mVerticalControlsParams == null) { - mVerticalControlsParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, mControlsAnimateHeight); - } - mVerticalControlsParams.height = Math.max(0, mControlsAnimateHeight - controlsOffset); - mMiniMonthContainer.setLayoutParams(mVerticalControlsParams); - } - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { - if (key.equals(GeneralPreferences.KEY_WEEK_START_DAY)) { - if (mPaused) { - mUpdateOnResume = true; - } else { - initFragments(mController.getTime(), mController.getViewType(), null); - } - } - } - - private void setMainPane( - FragmentTransaction ft, int viewId, int viewType, long timeMillis, boolean force) { - if (mOnSaveInstanceStateCalled) { - return; - } - if (!force && mCurrentView == viewType) { - return; - } - - // Remove this when transition to and from month view looks fine. - boolean doTransition = viewType != ViewType.MONTH && mCurrentView != ViewType.MONTH; - FragmentManager fragmentManager = getFragmentManager(); - - if (viewType != mCurrentView) { - // The rules for this previous view are different than the - // controller's and are used for intercepting the back button. - if (mCurrentView != ViewType.EDIT && mCurrentView > 0) { - mPreviousView = mCurrentView; - } - mCurrentView = viewType; - } - // Create new fragment - Fragment frag = null; - Fragment secFrag = null; - switch (viewType) { - case ViewType.AGENDA: - break; - case ViewType.DAY: - if (mActionBar != null && (mActionBar.getSelectedTab() != mDayTab)) { - mActionBar.selectTab(mDayTab); - } - if (mActionBarMenuSpinnerAdapter != null) { - mActionBar.setSelectedNavigationItem(CalendarViewAdapter.DAY_BUTTON_INDEX); - } - frag = new DayFragment(timeMillis, 1); - break; - case ViewType.MONTH: - if (mActionBar != null && (mActionBar.getSelectedTab() != mMonthTab)) { - mActionBar.selectTab(mMonthTab); - } - if (mActionBarMenuSpinnerAdapter != null) { - mActionBar.setSelectedNavigationItem(CalendarViewAdapter.MONTH_BUTTON_INDEX); - } - frag = new MonthByWeekFragment(timeMillis, false); - break; - case ViewType.WEEK: - default: - if (mActionBar != null && (mActionBar.getSelectedTab() != mWeekTab)) { - mActionBar.selectTab(mWeekTab); - } - if (mActionBarMenuSpinnerAdapter != null) { - mActionBar.setSelectedNavigationItem(CalendarViewAdapter.WEEK_BUTTON_INDEX); - } - frag = new DayFragment(timeMillis, 7); - break; - } - - // Update the current view so that the menu can update its look according to the - // current view. - if (mActionBarMenuSpinnerAdapter != null) { - mActionBarMenuSpinnerAdapter.setMainView(viewType); - if (!mIsTabletConfig) { - mActionBarMenuSpinnerAdapter.setTime(timeMillis); - } - } - - - // Show date only on tablet configurations in views different than Agenda - if (!mIsTabletConfig) { - mDateRange.setVisibility(View.GONE); - } else { - mDateRange.setVisibility(View.GONE); - } - - // Clear unnecessary buttons from the option menu when switching from the agenda view - if (viewType != ViewType.AGENDA) { - clearOptionsMenu(); - } - - boolean doCommit = false; - if (ft == null) { - doCommit = true; - ft = fragmentManager.beginTransaction(); - } - - if (doTransition) { - ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); - } - - ft.replace(viewId, frag); - if (DEBUG) { - Log.d(TAG, "Adding handler with viewId " + viewId + " and type " + viewType); - } - // If the key is already registered this will replace it - mController.registerEventHandler(viewId, (EventHandler) frag); - - if (doCommit) { - if (DEBUG) { - Log.d(TAG, "setMainPane AllInOne=" + this + " finishing:" + this.isFinishing()); - } - ft.commit(); - } - } - - private void setTitleInActionBar(EventInfo event) { - if (event.eventType != EventType.UPDATE_TITLE || mActionBar == null) { - return; - } - - final long start = event.startTime.toMillis(false /* use isDst */); - final long end; - if (event.endTime != null) { - end = event.endTime.toMillis(false /* use isDst */); - } else { - end = start; - } - - final String msg = Utils.formatDateRange(this, start, end, (int) event.extraLong); - CharSequence oldDate = mDateRange.getText(); - mDateRange.setText(msg); - updateSecondaryTitleFields(event.selectedTime != null ? event.selectedTime.toMillis(true) - : start); - if (!TextUtils.equals(oldDate, msg)) { - mDateRange.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - if (mShowWeekNum && mWeekTextView != null) { - mWeekTextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - } - } - } - - private void updateSecondaryTitleFields(long visibleMillisSinceEpoch) { - mShowWeekNum = Utils.getShowWeekNumber(this); - mTimeZone = Utils.getTimeZone(this, mHomeTimeUpdater); - if (visibleMillisSinceEpoch != -1) { - int weekNum = Utils.getWeekNumberFromTime(visibleMillisSinceEpoch, this); - mWeekNum = weekNum; - } - - if (mShowWeekNum && (mCurrentView == ViewType.WEEK) && mIsTabletConfig - && mWeekTextView != null) { - String weekString = getResources().getQuantityString(R.plurals.weekN, mWeekNum, - mWeekNum); - mWeekTextView.setText(weekString); - mWeekTextView.setVisibility(View.VISIBLE); - } else if (visibleMillisSinceEpoch != -1 && mWeekTextView != null - && mCurrentView == ViewType.DAY && mIsTabletConfig) { - Time time = new Time(mTimeZone); - time.set(visibleMillisSinceEpoch); - int julianDay = Time.getJulianDay(visibleMillisSinceEpoch, time.gmtoff); - time.setToNow(); - int todayJulianDay = Time.getJulianDay(time.toMillis(false), time.gmtoff); - String dayString = Utils.getDayOfWeekString(julianDay, todayJulianDay, - visibleMillisSinceEpoch, this); - mWeekTextView.setText(dayString); - mWeekTextView.setVisibility(View.VISIBLE); - } else if (mWeekTextView != null && (!mIsTabletConfig || mCurrentView != ViewType.DAY)) { - mWeekTextView.setVisibility(View.GONE); - } - - if (mHomeTime != null - && (mCurrentView == ViewType.DAY || mCurrentView == ViewType.WEEK) - && !TextUtils.equals(mTimeZone, Time.getCurrentTimezone())) { - Time time = new Time(mTimeZone); - time.setToNow(); - long millis = time.toMillis(true); - boolean isDST = time.isDst != 0; - int flags = DateUtils.FORMAT_SHOW_TIME; - if (DateFormat.is24HourFormat(this)) { - flags |= DateUtils.FORMAT_24HOUR; - } - // Formats the time as - String timeString = (new StringBuilder( - Utils.formatDateRange(this, millis, millis, flags))).append(" ").append( - TimeZone.getTimeZone(mTimeZone).getDisplayName( - isDST, TimeZone.SHORT, Locale.getDefault())).toString(); - mHomeTime.setText(timeString); - mHomeTime.setVisibility(View.VISIBLE); - // Update when the minute changes - mHomeTime.removeCallbacks(mHomeTimeUpdater); - mHomeTime.postDelayed( - mHomeTimeUpdater, - DateUtils.MINUTE_IN_MILLIS - (millis % DateUtils.MINUTE_IN_MILLIS)); - } else if (mHomeTime != null) { - mHomeTime.setVisibility(View.GONE); - } - } - - @Override - public long getSupportedEventTypes() { - return EventType.GO_TO | EventType.UPDATE_TITLE; - } - - @Override - public void handleEvent(EventInfo event) { - long displayTime = -1; - if (event.eventType == EventType.GO_TO) { - if ((event.extraLong & CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS) != 0) { - mBackToPreviousView = true; - } else if (event.viewType != mController.getPreviousViewType() - && event.viewType != ViewType.EDIT) { - // Clear the flag is change to a different view type - mBackToPreviousView = false; - } - - setMainPane( - null, R.id.main_pane, event.viewType, event.startTime.toMillis(false), false); - if (mShowCalendarControls) { - int animationSize = (mOrientation == Configuration.ORIENTATION_LANDSCAPE) ? - mControlsAnimateWidth : mControlsAnimateHeight; - boolean noControlsView = event.viewType == ViewType.MONTH; - if (mControlsMenu != null) { - mControlsMenu.setVisible(!noControlsView); - mControlsMenu.setEnabled(!noControlsView); - } - if (noControlsView || mHideControls) { - // hide minimonth and calendar frag - mShowSideViews = false; - if (!mHideControls) { - final ObjectAnimator slideAnimation = ObjectAnimator.ofInt(this, - "controlsOffset", 0, animationSize); - slideAnimation.addListener(mSlideAnimationDoneListener); - slideAnimation.setDuration(mCalendarControlsAnimationTime); - ObjectAnimator.setFrameDelay(0); - slideAnimation.start(); - } else { - mMiniMonth.setVisibility(View.GONE); - mCalendarsList.setVisibility(View.GONE); - mMiniMonthContainer.setVisibility(View.GONE); - } - } else { - // show minimonth and calendar frag - mShowSideViews = true; - mMiniMonth.setVisibility(View.VISIBLE); - mCalendarsList.setVisibility(View.VISIBLE); - mMiniMonthContainer.setVisibility(View.VISIBLE); - if (!mHideControls && - (mController.getPreviousViewType() == ViewType.MONTH)) { - final ObjectAnimator slideAnimation = ObjectAnimator.ofInt(this, - "controlsOffset", animationSize, 0); - slideAnimation.setDuration(mCalendarControlsAnimationTime); - ObjectAnimator.setFrameDelay(0); - slideAnimation.start(); - } - } - } - displayTime = event.selectedTime != null ? event.selectedTime.toMillis(true) - : event.startTime.toMillis(true); - if (!mIsTabletConfig) { - mActionBarMenuSpinnerAdapter.setTime(displayTime); - } - } else if (event.eventType == EventType.UPDATE_TITLE) { - setTitleInActionBar(event); - if (!mIsTabletConfig) { - mActionBarMenuSpinnerAdapter.setTime(mController.getTime()); - } - } - updateSecondaryTitleFields(displayTime); - } - - @Override - public void eventsChanged() { - mController.sendEvent(this, EventType.EVENTS_CHANGED, null, null, -1, ViewType.CURRENT); - } - - @Override - public void onTabSelected(Tab tab, FragmentTransaction ft) { - Log.w(TAG, "TabSelected AllInOne=" + this + " finishing:" + this.isFinishing()); - if (tab == mDayTab && mCurrentView != ViewType.DAY) { - mController.sendEvent(this, EventType.GO_TO, null, null, -1, ViewType.DAY); - } else if (tab == mWeekTab && mCurrentView != ViewType.WEEK) { - mController.sendEvent(this, EventType.GO_TO, null, null, -1, ViewType.WEEK); - } else if (tab == mMonthTab && mCurrentView != ViewType.MONTH) { - mController.sendEvent(this, EventType.GO_TO, null, null, -1, ViewType.MONTH); - } else { - Log.w(TAG, "TabSelected event from unknown tab: " - + (tab == null ? "null" : tab.getText())); - Log.w(TAG, "CurrentView:" + mCurrentView + " Tab:" + tab.toString() + " Day:" + mDayTab - + " Week:" + mWeekTab + " Month:" + mMonthTab); - } - } - - @Override - public void onTabReselected(Tab tab, FragmentTransaction ft) { - } - - @Override - public void onTabUnselected(Tab tab, FragmentTransaction ft) { - } - - - @Override - public boolean onNavigationItemSelected(int itemPosition, long itemId) { - switch (itemPosition) { - case CalendarViewAdapter.DAY_BUTTON_INDEX: - if (mCurrentView != ViewType.DAY) { - mController.sendEvent(this, EventType.GO_TO, null, null, -1, ViewType.DAY); - } - break; - case CalendarViewAdapter.WEEK_BUTTON_INDEX: - if (mCurrentView != ViewType.WEEK) { - mController.sendEvent(this, EventType.GO_TO, null, null, -1, ViewType.WEEK); - } - break; - case CalendarViewAdapter.MONTH_BUTTON_INDEX: - if (mCurrentView != ViewType.MONTH) { - mController.sendEvent(this, EventType.GO_TO, null, null, -1, ViewType.MONTH); - } - break; - case CalendarViewAdapter.AGENDA_BUTTON_INDEX: - break; - default: - Log.w(TAG, "ItemSelected event from unknown button: " + itemPosition); - Log.w(TAG, "CurrentView:" + mCurrentView + " Button:" + itemPosition + - " Day:" + mDayTab + " Week:" + mWeekTab + " Month:" + mMonthTab); - break; - } - return false; - } -} diff --git a/src/com/android/calendar/AllInOneActivity.kt b/src/com/android/calendar/AllInOneActivity.kt new file mode 100644 index 00000000..6c2e825f --- /dev/null +++ b/src/com/android/calendar/AllInOneActivity.kt @@ -0,0 +1,1065 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.accounts.AccountManager +import android.accounts.AccountManagerCallback +import android.accounts.AccountManagerFuture +import android.animation.Animator +import android.animation.Animator.AnimatorListener +import android.animation.ObjectAnimator +import android.app.ActionBar +import android.app.ActionBar.Tab +import android.app.Activity +import android.app.Fragment +import android.app.FragmentManager +import android.app.FragmentTransaction +import android.content.AsyncQueryHandler +import android.content.ContentResolver +import android.content.Intent +import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.content.res.Configuration +import android.content.res.Resources +import android.database.ContentObserver +import android.database.Cursor +import android.graphics.drawable.LayerDrawable +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.provider.CalendarContract +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Events +import android.text.TextUtils +import android.text.format.DateFormat +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.accessibility.AccessibilityEvent +import android.widget.LinearLayout +import android.widget.RelativeLayout +import android.widget.RelativeLayout.LayoutParams +import android.widget.TextView +import com.android.calendar.CalendarController.EventHandler +import com.android.calendar.CalendarController.EventInfo +import com.android.calendar.CalendarController.EventType +import com.android.calendar.CalendarController.ViewType +import com.android.calendar.month.MonthByWeekFragment +import java.util.Locale +import java.util.TimeZone +import android.provider.CalendarContract.Attendees.ATTENDEE_STATUS +import android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY +import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME +import android.provider.CalendarContract.EXTRA_EVENT_END_TIME + +class AllInOneActivity : Activity(), EventHandler, OnSharedPreferenceChangeListener, + ActionBar.TabListener, ActionBar.OnNavigationListener { + private var mController: CalendarController? = null + private var mOnSaveInstanceStateCalled = false + private var mBackToPreviousView = false + private var mContentResolver: ContentResolver? = null + private var mPreviousView = 0 + private var mCurrentView = 0 + private var mPaused = true + private var mUpdateOnResume = false + private var mHideControls = false + private var mShowSideViews = true + private var mShowWeekNum = false + private var mHomeTime: TextView? = null + private var mDateRange: TextView? = null + private var mWeekTextView: TextView? = null + private var mMiniMonth: View? = null + private var mCalendarsList: View? = null + private var mMiniMonthContainer: View? = null + private var mSecondaryPane: View? = null + private var mTimeZone: String? = null + private var mShowCalendarControls = false + private var mShowEventInfoFullScreen = false + private var mWeekNum = 0 + private var mCalendarControlsAnimationTime = 0 + private var mControlsAnimateWidth = 0 + private var mControlsAnimateHeight = 0 + private var mViewEventId: Long = -1 + private var mIntentEventStartMillis: Long = -1 + private var mIntentEventEndMillis: Long = -1 + private var mIntentAttendeeResponse: Int = Attendees.ATTENDEE_STATUS_NONE + private var mIntentAllDay = false + + // Action bar and Navigation bar (left side of Action bar) + private var mActionBar: ActionBar? = null + private val mDayTab: Tab? = null + private val mWeekTab: Tab? = null + private val mMonthTab: Tab? = null + private var mControlsMenu: MenuItem? = null + private var mOptionsMenu: Menu? = null + private var mActionBarMenuSpinnerAdapter: CalendarViewAdapter? = null + private var mHandler: QueryHandler? = null + private var mCheckForAccounts = true + private var mHideString: String? = null + private var mShowString: String? = null + var mDayOfMonthIcon: DayOfMonthDrawable? = null + var mOrientation = 0 + + // Params for animating the controls on the right + private var mControlsParams: LayoutParams? = null + private var mVerticalControlsParams: LinearLayout.LayoutParams? = null + private val mSlideAnimationDoneListener: AnimatorListener = object : AnimatorListener { + @Override + override fun onAnimationCancel(animation: Animator) { + } + + @Override + override fun onAnimationEnd(animation: Animator) { + val visibility: Int = if (mShowSideViews) View.VISIBLE else View.GONE + mMiniMonth?.setVisibility(visibility) + mCalendarsList?.setVisibility(visibility) + mMiniMonthContainer?.setVisibility(visibility) + } + + @Override + override fun onAnimationRepeat(animation: Animator) { + } + + @Override + override fun onAnimationStart(animation: Animator) { + } + } + + private inner class QueryHandler(cr: ContentResolver?) : AsyncQueryHandler(cr) { + @Override + protected override fun onQueryComplete(token: Int, cookie: Any?, cursor: Cursor?) { + mCheckForAccounts = false + try { + // If the query didn't return a cursor for some reason return + if (cursor == null || cursor.getCount() > 0 || isFinishing()) { + return + } + } finally { + if (cursor != null) { + cursor.close() + } + } + val options = Bundle() + options.putCharSequence( + "introMessage", + getResources().getString(R.string.create_an_account_desc) + ) + options.putBoolean("allowSkip", true) + val am: AccountManager = AccountManager.get(this@AllInOneActivity) + am.addAccount("com.google", CalendarContract.AUTHORITY, null, options, + this@AllInOneActivity, + object : AccountManagerCallback<Bundle?> { + @Override + override fun run(future: AccountManagerFuture<Bundle?>?) { + } + }, null + ) + } + } + + private val mHomeTimeUpdater: Runnable = object : Runnable { + @Override + override fun run() { + mTimeZone = Utils.getTimeZone(this@AllInOneActivity, this) + updateSecondaryTitleFields(-1) + this@AllInOneActivity.invalidateOptionsMenu() + Utils.setMidnightUpdater(mHandler, mTimeChangesUpdater, mTimeZone) + } + } + + // runs every midnight/time changes and refreshes the today icon + private val mTimeChangesUpdater: Runnable = object : Runnable { + @Override + override fun run() { + mTimeZone = Utils.getTimeZone(this@AllInOneActivity, mHomeTimeUpdater) + this@AllInOneActivity.invalidateOptionsMenu() + Utils.setMidnightUpdater(mHandler, this, mTimeZone) + } + } + + // Create an observer so that we can update the views whenever a + // Calendar event changes. + private val mObserver: ContentObserver = object : ContentObserver(Handler()) { + @Override + override fun deliverSelfNotifications(): Boolean { + return true + } + + @Override + override fun onChange(selfChange: Boolean) { + eventsChanged() + } + } + + @Override + protected override fun onNewIntent(intent: Intent) { + val action: String? = intent.getAction() + if (DEBUG) Log.d(TAG, "New intent received " + intent.toString()) + // Don't change the date if we're just returning to the app's home + if (Intent.ACTION_VIEW.equals(action) && + !intent.getBooleanExtra(Utils.INTENT_KEY_HOME, false) + ) { + var millis = parseViewAction(intent) + if (millis == -1L) { + millis = Utils.timeFromIntentInMillis(intent) as Long + } + if (millis != -1L && mViewEventId == -1L && mController != null) { + val time = Time(mTimeZone) + time.set(millis) + time.normalize(true) + mController?.sendEvent(this as Object?, EventType.GO_TO, time, time, -1, + ViewType.CURRENT) + } + } + } + + @Override + protected override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + if (icicle != null && icicle.containsKey(BUNDLE_KEY_CHECK_ACCOUNTS)) { + mCheckForAccounts = icicle.getBoolean(BUNDLE_KEY_CHECK_ACCOUNTS) + } + // Launch add google account if this is first time and there are no + // accounts yet + if (mCheckForAccounts) { + mHandler = QueryHandler(this.getContentResolver()) + mHandler?.startQuery( + 0, null, Calendars.CONTENT_URI, arrayOf<String>( + Calendars._ID + ), null, null /* selection args */, null /* sort order */ + ) + } + + // This needs to be created before setContentView + mController = CalendarController.getInstance(this) + + // Get time from intent or icicle + var timeMillis: Long = -1 + var viewType = -1 + val intent: Intent = getIntent() + if (icicle != null) { + timeMillis = icicle.getLong(BUNDLE_KEY_RESTORE_TIME) + viewType = icicle.getInt(BUNDLE_KEY_RESTORE_VIEW, -1) + } else { + val action: String? = intent.getAction() + if (Intent.ACTION_VIEW.equals(action)) { + // Open EventInfo later + timeMillis = parseViewAction(intent) + } + if (timeMillis == -1L) { + timeMillis = Utils.timeFromIntentInMillis(intent) as Long + } + } + if (viewType == -1 || viewType > ViewType.MAX_VALUE) { + viewType = Utils.getViewTypeFromIntentAndSharedPref(this) + } + mTimeZone = Utils.getTimeZone(this, mHomeTimeUpdater) + val t = Time(mTimeZone) + t.set(timeMillis) + if (DEBUG) { + if (icicle != null && intent != null) { + Log.d( + TAG, + "both, icicle:" + icicle.toString().toString() + " intent:" + intent.toString() + ) + } else { + Log.d(TAG, "not both, icicle:$icicle intent:$intent") + } + } + val res: Resources = getResources() + mHideString = res.getString(R.string.hide_controls) + mShowString = res.getString(R.string.show_controls) + mOrientation = res.getConfiguration().orientation + if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) { + mControlsAnimateWidth = res.getDimension(R.dimen.calendar_controls_width).toInt() + if (mControlsParams == null) { + mControlsParams = LayoutParams(mControlsAnimateWidth, 0) + } + mControlsParams?.addRule(RelativeLayout.ALIGN_PARENT_RIGHT) + as RelativeLayout.LayoutParams + } else { + // Make sure width is in between allowed min and max width values + mControlsAnimateWidth = Math.max( + res.getDisplayMetrics().widthPixels * 45 / 100, + res.getDimension(R.dimen.min_portrait_calendar_controls_width).toInt() + ) + mControlsAnimateWidth = Math.min( + mControlsAnimateWidth, + res.getDimension(R.dimen.max_portrait_calendar_controls_width).toInt() + ) + } + mControlsAnimateHeight = res?.getDimension(R.dimen.calendar_controls_height).toInt() + mHideControls = true + mIsMultipane = Utils.getConfigBool(this, R.bool.multiple_pane_config) + mIsTabletConfig = Utils.getConfigBool(this, R.bool.tablet_config) + mShowCalendarControls = Utils.getConfigBool(this, R.bool.show_calendar_controls) + mShowEventInfoFullScreen = Utils.getConfigBool(this, R.bool.show_event_info_full_screen) + mCalendarControlsAnimationTime = res.getInteger(R.integer.calendar_controls_animation_time) + Utils.setAllowWeekForDetailView(mIsMultipane) + + // setContentView must be called before configureActionBar + setContentView(R.layout.all_in_one) + if (mIsTabletConfig) { + mDateRange = findViewById(R.id.date_bar) as TextView? + mWeekTextView = findViewById(R.id.week_num) as TextView? + } else { + mDateRange = getLayoutInflater().inflate(R.layout.date_range_title, null) as TextView + } + + // configureActionBar auto-selects the first tab you add, so we need to + // call it before we set up our own fragments to make sure it doesn't + // overwrite us + configureActionBar(viewType) + mHomeTime = findViewById(R.id.home_time) as TextView? + mMiniMonth = findViewById(R.id.mini_month) + if (mIsTabletConfig && mOrientation == Configuration.ORIENTATION_PORTRAIT) { + mMiniMonth?.setLayoutParams( + LayoutParams( + mControlsAnimateWidth, + mControlsAnimateHeight + ) + ) + } + mCalendarsList = findViewById(R.id.calendar_list) + mMiniMonthContainer = findViewById(R.id.mini_month_container) + mSecondaryPane = findViewById(R.id.secondary_pane) + + // Must register as the first activity because this activity can modify + // the list of event handlers in it's handle method. This affects who + // the rest of the handlers the controller dispatches to are. + mController?.registerFirstEventHandler(HANDLER_KEY, this) + initFragments(timeMillis, viewType, icicle) + + // Listen for changes that would require this to be refreshed + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(this) + prefs?.registerOnSharedPreferenceChangeListener(this) + mContentResolver = getContentResolver() + } + + private fun parseViewAction(intent: Intent?): Long { + var timeMillis: Long = -1 + val data: Uri? = intent?.getData() + if (data != null && data?.isHierarchical()) { + val path = data.getPathSegments() + if (path?.size == 2 && path!![0].equals("events")) { + try { + mViewEventId = data.getLastPathSegment()?.toLong() as Long + if (mViewEventId != -1L) { + mIntentEventStartMillis = intent?.getLongExtra(EXTRA_EVENT_BEGIN_TIME, 0) + mIntentEventEndMillis = intent?.getLongExtra(EXTRA_EVENT_END_TIME, 0) + mIntentAttendeeResponse = intent?.getIntExtra( + ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE + ) + mIntentAllDay = intent?.getBooleanExtra(EXTRA_EVENT_ALL_DAY, false) + as Boolean + timeMillis = mIntentEventStartMillis + } + } catch (e: NumberFormatException) { + // Ignore if mViewEventId can't be parsed + } + } + } + return timeMillis + } + + private fun configureActionBar(viewType: Int) { + createButtonsSpinner(viewType, mIsTabletConfig) + if (mIsMultipane) { + mActionBar?.setDisplayOptions( + ActionBar.DISPLAY_SHOW_CUSTOM or ActionBar.DISPLAY_SHOW_HOME + ) + } else { + mActionBar?.setDisplayOptions(0) + } + } + + private fun createButtonsSpinner(viewType: Int, tabletConfig: Boolean) { + // If tablet configuration , show spinner with no dates + mActionBarMenuSpinnerAdapter = CalendarViewAdapter(this, viewType, !tabletConfig) + mActionBar = getActionBar() + mActionBar?.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST) + mActionBar?.setListNavigationCallbacks(mActionBarMenuSpinnerAdapter, this) + when (viewType) { + ViewType.AGENDA -> { + } + ViewType.DAY -> mActionBar?.setSelectedNavigationItem(BUTTON_DAY_INDEX) + ViewType.WEEK -> mActionBar?.setSelectedNavigationItem(BUTTON_WEEK_INDEX) + ViewType.MONTH -> mActionBar?.setSelectedNavigationItem(BUTTON_MONTH_INDEX) + else -> mActionBar?.setSelectedNavigationItem(BUTTON_DAY_INDEX) + } + } + + // Clear buttons used in the agenda view + private fun clearOptionsMenu() { + if (mOptionsMenu == null) { + return + } + val cancelItem: MenuItem? = mOptionsMenu?.findItem(R.id.action_cancel) + if (cancelItem != null) { + cancelItem?.setVisible(false) + } + } + + @Override + protected override fun onResume() { + super.onResume() + + // Check if the upgrade code has ever been run. If not, force a sync just this one time. + Utils.trySyncAndDisableUpgradeReceiver(this) + + // Must register as the first activity because this activity can modify + // the list of event handlers in it's handle method. This affects who + // the rest of the handlers the controller dispatches to are. + mController?.registerFirstEventHandler(HANDLER_KEY, this) + mOnSaveInstanceStateCalled = false + mContentResolver?.registerContentObserver( + CalendarContract.Events.CONTENT_URI, + true, mObserver + ) + if (mUpdateOnResume) { + initFragments(mController?.time as Long, mController?.viewType as Int, null) + mUpdateOnResume = false + } + val t = Time(mTimeZone) + t.set(mController?.time as Long) + mController?.sendEvent( + this as Object?, EventType.UPDATE_TITLE, t, t, -1, ViewType.CURRENT, + mController?.dateFlags as Long, null, null + ) + // Make sure the drop-down menu will get its date updated at midnight + if (mActionBarMenuSpinnerAdapter != null) { + mActionBarMenuSpinnerAdapter?.refresh(this) + } + if (mControlsMenu != null) { + mControlsMenu?.setTitle(if (mHideControls) mShowString else mHideString) + } + mPaused = false + if (mViewEventId != -1L && mIntentEventStartMillis != -1L && mIntentEventEndMillis != -1L) { + val currentMillis: Long = System.currentTimeMillis() + var selectedTime: Long = -1 + if (currentMillis > mIntentEventStartMillis && currentMillis < mIntentEventEndMillis) { + selectedTime = currentMillis + } + mController?.sendEventRelatedEventWithExtra( + this as Object?, EventType.VIEW_EVENT, mViewEventId, + mIntentEventStartMillis, mIntentEventEndMillis, -1, -1, + EventInfo.buildViewExtraLong(mIntentAttendeeResponse, mIntentAllDay), + selectedTime + ) + mViewEventId = -1 + mIntentEventStartMillis = -1 + mIntentEventEndMillis = -1 + mIntentAllDay = false + } + Utils.setMidnightUpdater(mHandler, mTimeChangesUpdater, mTimeZone) + // Make sure the today icon is up to date + invalidateOptionsMenu() + } + + @Override + protected override fun onPause() { + super.onPause() + mController?.deregisterEventHandler(HANDLER_KEY) + mPaused = true + mHomeTime?.removeCallbacks(mHomeTimeUpdater) + if (mActionBarMenuSpinnerAdapter != null) { + mActionBarMenuSpinnerAdapter?.onPause() + } + mContentResolver?.unregisterContentObserver(mObserver) + if (isFinishing()) { + // Stop listening for changes that would require this to be refreshed + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(this) + prefs?.unregisterOnSharedPreferenceChangeListener(this) + } + // FRAG_TODO save highlighted days of the week; + if (mController?.viewType != ViewType.EDIT) { + Utils.setDefaultView(this, mController?.viewType as Int) + } + Utils.resetMidnightUpdater(mHandler, mTimeChangesUpdater) + } + + @Override + protected override fun onUserLeaveHint() { + mController?.sendEvent(this as Object?, EventType.USER_HOME, null, null, -1, + ViewType.CURRENT) + super.onUserLeaveHint() + } + + @Override + override fun onSaveInstanceState(outState: Bundle) { + mOnSaveInstanceStateCalled = true + super.onSaveInstanceState(outState) + } + + @Override + protected override fun onDestroy() { + super.onDestroy() + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(this) + prefs?.unregisterOnSharedPreferenceChangeListener(this) + mController?.deregisterAllEventHandlers() + CalendarController.removeInstance(this) + } + + private fun initFragments(timeMillis: Long, viewType: Int, icicle: Bundle?) { + if (DEBUG) { + Log.d(TAG, "Initializing to $timeMillis for view $viewType") + } + val ft: FragmentTransaction = getFragmentManager().beginTransaction() + if (mShowCalendarControls) { + val miniMonthFrag: Fragment = MonthByWeekFragment(timeMillis, true) + ft.replace(R.id.mini_month, miniMonthFrag) + mController?.registerEventHandler(R.id.mini_month, miniMonthFrag as EventHandler) + } + if (!mShowCalendarControls || viewType == ViewType.EDIT) { + mMiniMonth?.setVisibility(View.GONE) + mCalendarsList?.setVisibility(View.GONE) + } + var info: EventInfo? = null + if (viewType == ViewType.EDIT) { + mPreviousView = GeneralPreferences.getSharedPreferences(this)?.getInt( + GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW + ) as Int + var eventId: Long = -1 + val intent: Intent = getIntent() + val data: Uri? = intent.getData() + if (data != null) { + try { + eventId = data?.getLastPathSegment()?.toLong() as Long + } catch (e: NumberFormatException) { + if (DEBUG) { + Log.d(TAG, "Create new event") + } + } + } else if (icicle != null && icicle.containsKey(BUNDLE_KEY_EVENT_ID)) { + eventId = icicle.getLong(BUNDLE_KEY_EVENT_ID) + } + val begin: Long = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, -1) + val end: Long = intent.getLongExtra(EXTRA_EVENT_END_TIME, -1) + info = EventInfo() + if (end != -1L) { + info?.endTime = Time() + info?.endTime?.set(end) + } + if (begin != -1L) { + info?.startTime = Time() + info?.startTime?.set(begin) + } + info.id = eventId + // We set the viewtype so if the user presses back when they are + // done editing the controller knows we were in the Edit Event + // screen. Likewise for eventId + mController?.viewType = viewType + mController?.eventId = eventId + } else { + mPreviousView = viewType + } + setMainPane(ft, R.id.main_pane, viewType, timeMillis, true) + ft.commit() // this needs to be after setMainPane() + val t = Time(mTimeZone) + t.set(timeMillis) + if (viewType != ViewType.EDIT) { + mController?.sendEvent(this as Object?, EventType.GO_TO, t, null, -1, viewType) + } + } + + @Override + override fun onBackPressed() { + if (mCurrentView == ViewType.EDIT || mBackToPreviousView) { + mController?.sendEvent(this as Object?, EventType.GO_TO, null, null, -1, mPreviousView) + } else { + super.onBackPressed() + } + } + + @Override + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + mOptionsMenu = menu + getMenuInflater().inflate(R.menu.all_in_one_title_bar, menu) + + // Hide the "show/hide controls" button if this is a phone + // or the view type is "Month". + mControlsMenu = menu.findItem(R.id.action_hide_controls) + if (!mShowCalendarControls) { + if (mControlsMenu != null) { + mControlsMenu?.setVisible(false) + mControlsMenu?.setEnabled(false) + } + } else if (mControlsMenu != null && mController != null && + mController?.viewType == ViewType.MONTH) { + mControlsMenu?.setVisible(false) + mControlsMenu?.setEnabled(false) + } else if (mControlsMenu != null) { + mControlsMenu?.setTitle(if (mHideControls) mShowString else mHideString) + } + val menuItem: MenuItem = menu.findItem(R.id.action_today) + if (Utils.isJellybeanOrLater()) { + // replace the default top layer drawable of the today icon with a + // custom drawable that shows the day of the month of today + val icon: LayerDrawable = menuItem.getIcon() as LayerDrawable + Utils.setTodayIcon(icon, this, mTimeZone) + } else { + menuItem.setIcon(R.drawable.ic_menu_today_no_date_holo_light) + } + return true + } + + @Override + override fun onOptionsItemSelected(item: MenuItem): Boolean { + var t: Time? = null + var viewType: Int = ViewType.CURRENT + var extras: Long = CalendarController.EXTRA_GOTO_TIME + val itemId: Int = item.getItemId() + if (itemId == R.id.action_today) { + viewType = ViewType.CURRENT + t = Time(mTimeZone) + t.setToNow() + extras = extras or CalendarController.EXTRA_GOTO_TODAY + } else if (itemId == R.id.action_hide_controls) { + mHideControls = !mHideControls + item.setTitle(if (mHideControls) mShowString else mHideString) + if (!mHideControls) { + mMiniMonth?.setVisibility(View.VISIBLE) + mCalendarsList?.setVisibility(View.VISIBLE) + mMiniMonthContainer?.setVisibility(View.VISIBLE) + } + val slideAnimation: ObjectAnimator = ObjectAnimator.ofInt( + this, "controlsOffset", + if (mHideControls) 0 else mControlsAnimateWidth, + if (mHideControls) mControlsAnimateWidth else 0 + ) + slideAnimation.setDuration(mCalendarControlsAnimationTime.toLong()) + ObjectAnimator.setFrameDelay(0) + slideAnimation.start() + return true + } else { + Log.d(TAG, "Unsupported itemId: $itemId") + return true + } + mController?.sendEvent(this as Object?, EventType.GO_TO, t, null, t, -1, + viewType, extras, null, null) + return true + } + + /** + * Sets the offset of the controls on the right for animating them off/on + * screen. ProGuard strips this if it's not in proguard.flags + * + * @param controlsOffset The current offset in pixels + */ + fun setControlsOffset(controlsOffset: Int) { + if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) { + mMiniMonth?.setTranslationX(controlsOffset.toFloat()) + mCalendarsList?.setTranslationX(controlsOffset.toFloat()) + mControlsParams?.width = Math.max(0, mControlsAnimateWidth - controlsOffset) + mMiniMonthContainer?.setLayoutParams(mControlsParams) + } else { + mMiniMonth?.setTranslationY(controlsOffset.toFloat()) + mCalendarsList?.setTranslationY(controlsOffset.toFloat()) + if (mVerticalControlsParams == null) { + mVerticalControlsParams = LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, mControlsAnimateHeight + ) as LinearLayout.LayoutParams? + } + mVerticalControlsParams?.height = Math.max(0, mControlsAnimateHeight - controlsOffset) + mMiniMonthContainer?.setLayoutParams(mVerticalControlsParams) + } + } + + @Override + override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String) { + if (key.equals(GeneralPreferences.KEY_WEEK_START_DAY)) { + if (mPaused) { + mUpdateOnResume = true + } else { + initFragments(mController?.time as Long, mController?.viewType as Int, null) + } + } + } + + private fun setMainPane( + ft: FragmentTransaction?, + viewId: Int, + viewType: Int, + timeMillis: Long, + force: Boolean + ) { + var ft: FragmentTransaction? = ft + if (mOnSaveInstanceStateCalled) { + return + } + if (!force && mCurrentView == viewType) { + return + } + + // Remove this when transition to and from month view looks fine. + val doTransition = viewType != ViewType.MONTH && mCurrentView != ViewType.MONTH + val fragmentManager: FragmentManager = getFragmentManager() + if (viewType != mCurrentView) { + // The rules for this previous view are different than the + // controller's and are used for intercepting the back button. + if (mCurrentView != ViewType.EDIT && mCurrentView > 0) { + mPreviousView = mCurrentView + } + mCurrentView = viewType + } + // Create new fragment + var frag: Fragment? = null + val secFrag: Fragment? = null + when (viewType) { + ViewType.AGENDA -> { + } + ViewType.DAY -> { + if (mActionBar != null && mActionBar?.getSelectedTab() != mDayTab) { + mActionBar?.selectTab(mDayTab) + } + if (mActionBarMenuSpinnerAdapter != null) { + mActionBar?.setSelectedNavigationItem(CalendarViewAdapter.DAY_BUTTON_INDEX) + } + frag = DayFragment(timeMillis, 1) + } + ViewType.MONTH -> { + if (mActionBar != null && mActionBar?.getSelectedTab() != mMonthTab) { + mActionBar?.selectTab(mMonthTab) + } + if (mActionBarMenuSpinnerAdapter != null) { + mActionBar?.setSelectedNavigationItem(CalendarViewAdapter.MONTH_BUTTON_INDEX) + } + frag = MonthByWeekFragment(timeMillis, false) + } + ViewType.WEEK -> { + if (mActionBar != null && mActionBar?.getSelectedTab() != mWeekTab) { + mActionBar?.selectTab(mWeekTab) + } + if (mActionBarMenuSpinnerAdapter != null) { + mActionBar?.setSelectedNavigationItem(CalendarViewAdapter.WEEK_BUTTON_INDEX) + } + frag = DayFragment(timeMillis, 7) + } + else -> { + if (mActionBar != null && mActionBar?.getSelectedTab() != mWeekTab) { + mActionBar?.selectTab(mWeekTab) + } + if (mActionBarMenuSpinnerAdapter != null) { + mActionBar?.setSelectedNavigationItem(CalendarViewAdapter.WEEK_BUTTON_INDEX) + } + frag = DayFragment(timeMillis, 7) + } + } + + // Update the current view so that the menu can update its look according to the + // current view. + if (mActionBarMenuSpinnerAdapter != null) { + mActionBarMenuSpinnerAdapter?.setMainView(viewType) + if (!mIsTabletConfig) { + mActionBarMenuSpinnerAdapter?.setTime(timeMillis) + } + } + + // Show date only on tablet configurations in views different than Agenda + if (!mIsTabletConfig) { + mDateRange?.setVisibility(View.GONE) + } else { + mDateRange?.setVisibility(View.GONE) + } + + // Clear unnecessary buttons from the option menu when switching from the agenda view + if (viewType != ViewType.AGENDA) { + clearOptionsMenu() + } + var doCommit = false + if (ft == null) { + doCommit = true + ft = fragmentManager.beginTransaction() + } + if (doTransition) { + ft?.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + } + ft?.replace(viewId, frag) + if (DEBUG) { + Log.d(TAG, "Adding handler with viewId $viewId and type $viewType") + } + // If the key is already registered this will replace it + mController?.registerEventHandler(viewId, frag as EventHandler?) + if (doCommit) { + if (DEBUG) { + Log.d(TAG, "setMainPane AllInOne=" + this + " finishing:" + this.isFinishing()) + } + ft?.commit() + } + } + + private fun setTitleInActionBar(event: EventInfo) { + if (event.eventType != EventType.UPDATE_TITLE || mActionBar == null) { + return + } + val start: Long? = event?.startTime?.toMillis(false /* use isDst */) + val end: Long? + end = if (event.endTime != null) { + event?.endTime?.toMillis(false /* use isDst */) + } else { + start + } + val msg: String? = Utils.formatDateRange(this, + start as Long, + end as Long, + event.extraLong.toInt() + ) + val oldDate: CharSequence? = mDateRange?.getText() + mDateRange?.setText(msg) + updateSecondaryTitleFields(if (event?.selectedTime != null) + event?.selectedTime?.toMillis(true) as Long else start) + if (!TextUtils.equals(oldDate, msg)) { + mDateRange?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + if (mShowWeekNum && mWeekTextView != null) { + mWeekTextView?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } + } + + private fun updateSecondaryTitleFields(visibleMillisSinceEpoch: Long) { + mShowWeekNum = Utils.getShowWeekNumber(this) + mTimeZone = Utils.getTimeZone(this, mHomeTimeUpdater) + if (visibleMillisSinceEpoch != -1L) { + val weekNum: Int = Utils.getWeekNumberFromTime(visibleMillisSinceEpoch, this) + mWeekNum = weekNum + } + if (mShowWeekNum && mCurrentView == ViewType.WEEK && mIsTabletConfig && + mWeekTextView != null + ) { + val weekString: String = getResources().getQuantityString( + R.plurals.weekN, mWeekNum, + mWeekNum + ) + mWeekTextView?.setText(weekString) + mWeekTextView?.setVisibility(View.VISIBLE) + } else if (visibleMillisSinceEpoch != -1L && mWeekTextView != null && + mCurrentView == ViewType.DAY && mIsTabletConfig) { + val time = Time(mTimeZone) + time.set(visibleMillisSinceEpoch) + val julianDay: Int = Time.getJulianDay(visibleMillisSinceEpoch, time.gmtoff) + time.setToNow() + val todayJulianDay: Int = Time.getJulianDay(time.toMillis(false), time.gmtoff) + val dayString: String = Utils.getDayOfWeekString( + julianDay, + todayJulianDay, + visibleMillisSinceEpoch, + this + ) + mWeekTextView?.setText(dayString) + mWeekTextView?.setVisibility(View.VISIBLE) + } else if (mWeekTextView != null && (!mIsTabletConfig || mCurrentView != ViewType.DAY)) { + mWeekTextView?.setVisibility(View.GONE) + } + if (mHomeTime != null && (mCurrentView == ViewType.DAY || mCurrentView == ViewType.WEEK) && + !TextUtils.equals(mTimeZone, Time.getCurrentTimezone()) + ) { + val time = Time(mTimeZone) + time.setToNow() + val millis: Long = time.toMillis(true) + val isDST = time.isDst !== 0 + var flags: Int = DateUtils.FORMAT_SHOW_TIME + if (DateFormat.is24HourFormat(this)) { + flags = flags or DateUtils.FORMAT_24HOUR + } + // Formats the time as + val timeString: String = StringBuilder( + Utils.formatDateRange(this, millis, millis, flags) + ).append(" ").append( + TimeZone.getTimeZone(mTimeZone).getDisplayName( + isDST, TimeZone.SHORT, Locale.getDefault() + ) + ).toString() + mHomeTime?.setText(timeString) + mHomeTime?.setVisibility(View.VISIBLE) + // Update when the minute changes + mHomeTime?.removeCallbacks(mHomeTimeUpdater) + mHomeTime?.postDelayed( + mHomeTimeUpdater, + DateUtils.MINUTE_IN_MILLIS - millis % DateUtils.MINUTE_IN_MILLIS + ) + } else if (mHomeTime != null) { + mHomeTime?.setVisibility(View.GONE) + } + } + + @get:Override override val supportedEventTypes: Long + get() = EventType.GO_TO or EventType.UPDATE_TITLE + + @Override + override fun handleEvent(event: EventInfo?) { + var displayTime: Long = -1 + if (event?.eventType == EventType.GO_TO) { + if (event?.extraLong and CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS != 0L) { + mBackToPreviousView = true + } else if (event?.viewType != mController?.previousViewType && + event?.viewType != ViewType.EDIT + ) { + // Clear the flag is change to a different view type + mBackToPreviousView = false + } + setMainPane( + null, R.id.main_pane, event?.viewType, event?.startTime?.toMillis(false) + as Long, false + ) + if (mShowCalendarControls) { + val animationSize = + if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) mControlsAnimateWidth + else mControlsAnimateHeight + val noControlsView = event?.viewType == ViewType.MONTH + if (mControlsMenu != null) { + mControlsMenu?.setVisible(!noControlsView) + mControlsMenu?.setEnabled(!noControlsView) + } + if (noControlsView || mHideControls) { + // hide minimonth and calendar frag + mShowSideViews = false + if (!mHideControls) { + val slideAnimation: ObjectAnimator = ObjectAnimator.ofInt( + this, + "controlsOffset", 0, animationSize + ) + slideAnimation.addListener(mSlideAnimationDoneListener) + slideAnimation.setDuration(mCalendarControlsAnimationTime.toLong()) + ObjectAnimator.setFrameDelay(0) + slideAnimation.start() + } else { + mMiniMonth?.setVisibility(View.GONE) + mCalendarsList?.setVisibility(View.GONE) + mMiniMonthContainer?.setVisibility(View.GONE) + } + } else { + // show minimonth and calendar frag + mShowSideViews = true + mMiniMonth?.setVisibility(View.VISIBLE) + mCalendarsList?.setVisibility(View.VISIBLE) + mMiniMonthContainer?.setVisibility(View.VISIBLE) + if (!mHideControls && + mController?.previousViewType == ViewType.MONTH + ) { + val slideAnimation: ObjectAnimator = ObjectAnimator.ofInt( + this, + "controlsOffset", animationSize, 0 + ) + slideAnimation.setDuration(mCalendarControlsAnimationTime.toLong()) + ObjectAnimator.setFrameDelay(0) + slideAnimation.start() + } + } + } + displayTime = + if (event?.selectedTime != null) event?.selectedTime?.toMillis(true) as Long + else event?.startTime?.toMillis(true) as Long + if (!mIsTabletConfig) { + mActionBarMenuSpinnerAdapter?.setTime(displayTime) + } + } else if (event?.eventType == EventType.UPDATE_TITLE) { + setTitleInActionBar(event as CalendarController.EventInfo) + if (!mIsTabletConfig) { + mActionBarMenuSpinnerAdapter?.setTime(mController?.time as Long) + } + } + updateSecondaryTitleFields(displayTime) + } + + @Override + override fun eventsChanged() { + mController?.sendEvent(this as Object?, EventType.EVENTS_CHANGED, null, null, -1, + ViewType.CURRENT) + } + + @Override + override fun onTabSelected(tab: Tab?, ft: FragmentTransaction?) { + Log.w(TAG, "TabSelected AllInOne=" + this + " finishing:" + this.isFinishing()) + if (tab == mDayTab && mCurrentView != ViewType.DAY) { + mController?.sendEvent(this as Object?, EventType.GO_TO, null, null, -1, ViewType.DAY) + } else if (tab == mWeekTab && mCurrentView != ViewType.WEEK) { + mController?.sendEvent(this as Object?, EventType.GO_TO, null, null, -1, ViewType.WEEK) + } else if (tab == mMonthTab && mCurrentView != ViewType.MONTH) { + mController?.sendEvent(this as Object?, EventType.GO_TO, null, null, -1, ViewType.MONTH) + } else { + Log.w( + TAG, "TabSelected event from unknown tab: " + + if (tab == null) "null" else tab.getText() + ) + Log.w( + TAG, "CurrentView:" + mCurrentView + " Tab:" + tab.toString() + " Day:" + mDayTab + + " Week:" + mWeekTab + " Month:" + mMonthTab + ) + } + } + + @Override + override fun onTabReselected(tab: Tab?, ft: FragmentTransaction?) { + } + + @Override + override fun onTabUnselected(tab: Tab?, ft: FragmentTransaction?) { + } + + @Override + override fun onNavigationItemSelected(itemPosition: Int, itemId: Long): Boolean { + when (itemPosition) { + CalendarViewAdapter.DAY_BUTTON_INDEX -> if (mCurrentView != ViewType.DAY) { + mController?.sendEvent(this as Object?, EventType.GO_TO, null, null, -1, + ViewType.DAY) + } + CalendarViewAdapter.WEEK_BUTTON_INDEX -> if (mCurrentView != ViewType.WEEK) { + mController?.sendEvent(this as Object?, EventType.GO_TO, null, null, -1, + ViewType.WEEK) + } + CalendarViewAdapter.MONTH_BUTTON_INDEX -> if (mCurrentView != ViewType.MONTH) { + mController?.sendEvent(this as Object?, EventType.GO_TO, null, null, -1, + ViewType.MONTH) + } + CalendarViewAdapter.AGENDA_BUTTON_INDEX -> { + } + else -> { + Log.w(TAG, "ItemSelected event from unknown button: $itemPosition") + Log.w( + TAG, "CurrentView:" + mCurrentView + " Button:" + itemPosition + + " Day:" + mDayTab + " Week:" + mWeekTab + " Month:" + mMonthTab + ) + } + } + return false + } + + companion object { + private const val TAG = "AllInOneActivity" + private const val DEBUG = false + private const val EVENT_INFO_FRAGMENT_TAG = "EventInfoFragment" + private const val BUNDLE_KEY_RESTORE_TIME = "key_restore_time" + private const val BUNDLE_KEY_EVENT_ID = "key_event_id" + private const val BUNDLE_KEY_RESTORE_VIEW = "key_restore_view" + private const val BUNDLE_KEY_CHECK_ACCOUNTS = "key_check_for_accounts" + private const val HANDLER_KEY = 0 + + // Indices of buttons for the drop down menu (tabs replacement) + // Must match the strings in the array buttons_list in arrays.xml and the + // OnNavigationListener + private const val BUTTON_DAY_INDEX = 0 + private const val BUTTON_WEEK_INDEX = 1 + private const val BUTTON_MONTH_INDEX = 2 + private const val BUTTON_AGENDA_INDEX = 3 + private var mIsMultipane = false + private var mIsTabletConfig = false + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/AsyncQueryServiceHelper.java b/src/com/android/calendar/AsyncQueryServiceHelper.java deleted file mode 100644 index c6e0a2bc..00000000 --- a/src/com/android/calendar/AsyncQueryServiceHelper.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar; - -import android.app.IntentService; -import android.content.ContentProviderOperation; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.net.Uri; -import android.os.Handler; -import android.os.Message; -import android.os.RemoteException; -import android.os.SystemClock; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.PriorityQueue; -import java.util.concurrent.Delayed; -import java.util.concurrent.TimeUnit; - -public class AsyncQueryServiceHelper extends IntentService { - private static final String TAG = "AsyncQuery"; - - public AsyncQueryServiceHelper(String name) { - super(name); - } - - public AsyncQueryServiceHelper() { - super("AsyncQueryServiceHelper"); - } - - @Override - protected void onHandleIntent(Intent intent) { - } - - @Override - public void onStart(Intent intent, int startId) { - super.onStart(intent, startId); - } - - @Override - public void onCreate() { - super.onCreate(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - } -} diff --git a/src/com/android/calendar/AsyncQueryServiceHelper.kt b/src/com/android/calendar/AsyncQueryServiceHelper.kt new file mode 100644 index 00000000..47973304 --- /dev/null +++ b/src/com/android/calendar/AsyncQueryServiceHelper.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.app.IntentService +import android.content.ContentProviderOperation +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.content.OperationApplicationException +import android.database.Cursor +import android.net.Uri +import android.os.Handler +import android.os.Message +import android.os.RemoteException +import android.os.SystemClock +import android.util.Log +import java.util.ArrayList +import java.util.Arrays +import java.util.Iterator +import java.util.PriorityQueue +import java.util.concurrent.Delayed +import java.util.concurrent.TimeUnit + +class AsyncQueryServiceHelper : IntentService { + constructor(name: String?) : super(name) {} + constructor() : super("AsyncQueryServiceHelper") {} + + protected override fun onHandleIntent(intent: Intent?) { + } + + override fun onStart(intent: Intent?, startId: Int) { + super.onStart(intent, startId) + } + + override fun onCreate() { + super.onCreate() + } + + override fun onDestroy() { + super.onDestroy() + } + + companion object { + private const val TAG = "AsyncQuery" + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/CalendarApplication.java b/src/com/android/calendar/CalendarApplication.kt index d0ca4698..445d7257 100644 --- a/src/com/android/calendar/CalendarApplication.java +++ b/src/com/android/calendar/CalendarApplication.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2021 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. @@ -13,20 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.calendar -package com.android.calendar; +import android.app.Application -import android.app.Application; - -public class CalendarApplication extends Application { - @Override - public void onCreate() { - super.onCreate(); +class CalendarApplication : Application() { + override fun onCreate() { + super.onCreate() /* * Ensure the default values are set for any receiver, activity, * service, etc. of Calendar */ - GeneralPreferences.setDefaultValues(this); + GeneralPreferences.setDefaultValues(this) } -} +}
\ No newline at end of file diff --git a/src/com/android/calendar/CalendarBackupAgent.java b/src/com/android/calendar/CalendarBackupAgent.java deleted file mode 100644 index 02456fdc..00000000 --- a/src/com/android/calendar/CalendarBackupAgent.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar; - -import android.app.backup.BackupAgentHelper; -import android.app.backup.BackupDataInput; -import android.app.backup.SharedPreferencesBackupHelper; -import android.content.Context; -import android.content.SharedPreferences.Editor; -import android.os.ParcelFileDescriptor; - -import java.io.IOException; - -public class CalendarBackupAgent extends BackupAgentHelper -{ - static final String SHARED_KEY = "shared_pref"; - - @Override - public void onCreate() { - } - - @Override - public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) - throws IOException { - super.onRestore(data, appVersionCode, newState); - } -} diff --git a/src/com/android/calendar/CalendarBackupAgent.kt b/src/com/android/calendar/CalendarBackupAgent.kt new file mode 100644 index 00000000..f3e230ac --- /dev/null +++ b/src/com/android/calendar/CalendarBackupAgent.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.app.backup.BackupAgentHelper +import android.app.backup.BackupDataInput +import android.app.backup.SharedPreferencesBackupHelper +import android.content.Context +import android.content.SharedPreferences.Editor +import android.os.ParcelFileDescriptor + +import java.io.IOException + +class CalendarBackupAgent : BackupAgentHelper() { + override fun onCreate() { + } + + @Throws(IOException::class) + override fun onRestore(data: BackupDataInput?, appVersionCode: Int, + newState: ParcelFileDescriptor?) { + super.onRestore(data, appVersionCode, newState) + } + + companion object { + const val SHARED_KEY = "shared_pref" + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/CalendarController.java b/src/com/android/calendar/CalendarController.java deleted file mode 100644 index 37286f2e..00000000 --- a/src/com/android/calendar/CalendarController.java +++ /dev/null @@ -1,713 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar; - -import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY; -import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; -import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; -import static android.provider.CalendarContract.Attendees.ATTENDEE_STATUS; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.app.Activity; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.provider.CalendarContract.Attendees; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Events; -import android.text.format.Time; -import android.util.Log; -import android.util.Pair; - -import java.lang.ref.WeakReference; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.Map.Entry; -import java.util.WeakHashMap; - -public class CalendarController { - private static final boolean DEBUG = false; - private static final String TAG = "CalendarController"; - - public static final String EVENT_EDIT_ON_LAUNCH = "editMode"; - - public static final int MIN_CALENDAR_YEAR = 1970; - public static final int MAX_CALENDAR_YEAR = 2036; - public static final int MIN_CALENDAR_WEEK = 0; - public static final int MAX_CALENDAR_WEEK = 3497; // weeks between 1/1/1970 and 1/1/2037 - - private final Context mContext; - - // This uses a LinkedHashMap so that we can replace fragments based on the - // view id they are being expanded into since we can't guarantee a reference - // to the handler will be findable - private final LinkedHashMap<Integer,EventHandler> eventHandlers = - new LinkedHashMap<Integer,EventHandler>(5); - private final LinkedList<Integer> mToBeRemovedEventHandlers = new LinkedList<Integer>(); - private final LinkedHashMap<Integer, EventHandler> mToBeAddedEventHandlers = new LinkedHashMap< - Integer, EventHandler>(); - private Pair<Integer, EventHandler> mFirstEventHandler; - private Pair<Integer, EventHandler> mToBeAddedFirstEventHandler; - private volatile int mDispatchInProgressCounter = 0; - - private static WeakHashMap<Context, WeakReference<CalendarController>> instances = - new WeakHashMap<Context, WeakReference<CalendarController>>(); - - private final WeakHashMap<Object, Long> filters = new WeakHashMap<Object, Long>(1); - - private int mViewType = -1; - private int mDetailViewType = -1; - private int mPreviousViewType = -1; - private long mEventId = -1; - private final Time mTime = new Time(); - private long mDateFlags = 0; - - private final Runnable mUpdateTimezone = new Runnable() { - @Override - public void run() { - mTime.switchTimezone(Utils.getTimeZone(mContext, this)); - } - }; - - /** - * One of the event types that are sent to or from the controller - */ - public interface EventType { - // Simple view of an event - final long VIEW_EVENT = 1L << 1; - - // Full detail view in read only mode - final long VIEW_EVENT_DETAILS = 1L << 2; - - // full detail view in edit mode - final long EDIT_EVENT = 1L << 3; - - final long GO_TO = 1L << 5; - - final long EVENTS_CHANGED = 1L << 7; - - final long USER_HOME = 1L << 9; - - // date range has changed, update the title - final long UPDATE_TITLE = 1L << 10; - } - - /** - * One of the Agenda/Day/Week/Month view types - */ - public interface ViewType { - final int DETAIL = -1; - final int CURRENT = 0; - final int AGENDA = 1; - final int DAY = 2; - final int WEEK = 3; - final int MONTH = 4; - final int EDIT = 5; - final int MAX_VALUE = 5; - } - - public static class EventInfo { - - private static final long ATTENTEE_STATUS_MASK = 0xFF; - private static final long ALL_DAY_MASK = 0x100; - private static final int ATTENDEE_STATUS_NONE_MASK = 0x01; - private static final int ATTENDEE_STATUS_ACCEPTED_MASK = 0x02; - private static final int ATTENDEE_STATUS_DECLINED_MASK = 0x04; - private static final int ATTENDEE_STATUS_TENTATIVE_MASK = 0x08; - - public long eventType; // one of the EventType - public int viewType; // one of the ViewType - public long id; // event id - public Time selectedTime; // the selected time in focus - - // Event start and end times. All-day events are represented in: - // - local time for GO_TO commands - // - UTC time for VIEW_EVENT and other event-related commands - public Time startTime; - public Time endTime; - - public int x; // x coordinate in the activity space - public int y; // y coordinate in the activity space - public String query; // query for a user search - public ComponentName componentName; // used in combination with query - public String eventTitle; - public long calendarId; - - /** - * For EventType.VIEW_EVENT: - * It is the default attendee response and an all day event indicator. - * Set to Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED, - * Attendees.ATTENDEE_STATUS_DECLINED, or Attendees.ATTENDEE_STATUS_TENTATIVE. - * To signal the event is an all-day event, "or" ALL_DAY_MASK with the response. - * Alternatively, use buildViewExtraLong(), getResponse(), and isAllDay(). - * <p> - * For EventType.GO_TO: - * Set to {@link #EXTRA_GOTO_TIME} to go to the specified date/time. - * Set to {@link #EXTRA_GOTO_DATE} to consider the date but ignore the time. - * Set to {@link #EXTRA_GOTO_BACK_TO_PREVIOUS} if back should bring back previous view. - * Set to {@link #EXTRA_GOTO_TODAY} if this is a user request to go to the current time. - * <p> - * For EventType.UPDATE_TITLE: - * Set formatting flags for Utils.formatDateRange - */ - public long extraLong; - - public boolean isAllDay() { - if (eventType != EventType.VIEW_EVENT) { - Log.wtf(TAG, "illegal call to isAllDay , wrong event type " + eventType); - return false; - } - return ((extraLong & ALL_DAY_MASK) != 0) ? true : false; - } - - public int getResponse() { - if (eventType != EventType.VIEW_EVENT) { - Log.wtf(TAG, "illegal call to getResponse , wrong event type " + eventType); - return Attendees.ATTENDEE_STATUS_NONE; - } - - int response = (int)(extraLong & ATTENTEE_STATUS_MASK); - switch (response) { - case ATTENDEE_STATUS_NONE_MASK: - return Attendees.ATTENDEE_STATUS_NONE; - case ATTENDEE_STATUS_ACCEPTED_MASK: - return Attendees.ATTENDEE_STATUS_ACCEPTED; - case ATTENDEE_STATUS_DECLINED_MASK: - return Attendees.ATTENDEE_STATUS_DECLINED; - case ATTENDEE_STATUS_TENTATIVE_MASK: - return Attendees.ATTENDEE_STATUS_TENTATIVE; - default: - Log.wtf(TAG,"Unknown attendee response " + response); - } - return ATTENDEE_STATUS_NONE_MASK; - } - - // Used to build the extra long for a VIEW event. - public static long buildViewExtraLong(int response, boolean allDay) { - long extra = allDay ? ALL_DAY_MASK : 0; - - switch (response) { - case Attendees.ATTENDEE_STATUS_NONE: - extra |= ATTENDEE_STATUS_NONE_MASK; - break; - case Attendees.ATTENDEE_STATUS_ACCEPTED: - extra |= ATTENDEE_STATUS_ACCEPTED_MASK; - break; - case Attendees.ATTENDEE_STATUS_DECLINED: - extra |= ATTENDEE_STATUS_DECLINED_MASK; - break; - case Attendees.ATTENDEE_STATUS_TENTATIVE: - extra |= ATTENDEE_STATUS_TENTATIVE_MASK; - break; - default: - Log.wtf(TAG,"Unknown attendee response " + response); - extra |= ATTENDEE_STATUS_NONE_MASK; - break; - } - return extra; - } - } - - /** - * Pass to the ExtraLong parameter for EventType.GO_TO to signal the time - * can be ignored - */ - public static final long EXTRA_GOTO_DATE = 1; - public static final long EXTRA_GOTO_TIME = 2; - public static final long EXTRA_GOTO_BACK_TO_PREVIOUS = 4; - public static final long EXTRA_GOTO_TODAY = 8; - - public interface EventHandler { - long getSupportedEventTypes(); - void handleEvent(EventInfo event); - - /** - * This notifies the handler that the database has changed and it should - * update its view. - */ - void eventsChanged(); - } - - /** - * Creates and/or returns an instance of CalendarController associated with - * the supplied context. It is best to pass in the current Activity. - * - * @param context The activity if at all possible. - */ - public static CalendarController getInstance(Context context) { - synchronized (instances) { - CalendarController controller = null; - WeakReference<CalendarController> weakController = instances.get(context); - if (weakController != null) { - controller = weakController.get(); - } - - if (controller == null) { - controller = new CalendarController(context); - instances.put(context, new WeakReference(controller)); - } - return controller; - } - } - - /** - * Removes an instance when it is no longer needed. This should be called in - * an activity's onDestroy method. - * - * @param context The activity used to create the controller - */ - public static void removeInstance(Context context) { - instances.remove(context); - } - - private CalendarController(Context context) { - mContext = context; - mUpdateTimezone.run(); - mTime.setToNow(); - mDetailViewType = Utils.getSharedPreference(mContext, - GeneralPreferences.KEY_DETAILED_VIEW, - GeneralPreferences.DEFAULT_DETAILED_VIEW); - } - - public void sendEventRelatedEvent(Object sender, long eventType, long eventId, long startMillis, - long endMillis, int x, int y, long selectedMillis) { - // TODO: pass the real allDay status or at least a status that says we don't know the - // status and have the receiver query the data. - // The current use of this method for VIEW_EVENT is by the day view to show an EventInfo - // so currently the missing allDay status has no effect. - sendEventRelatedEventWithExtra(sender, eventType, eventId, startMillis, endMillis, x, y, - EventInfo.buildViewExtraLong(Attendees.ATTENDEE_STATUS_NONE, false), - selectedMillis); - } - - /** - * Helper for sending New/View/Edit/Delete events - * - * @param sender object of the caller - * @param eventType one of {@link EventType} - * @param eventId event id - * @param startMillis start time - * @param endMillis end time - * @param x x coordinate in the activity space - * @param y y coordinate in the activity space - * @param extraLong default response value for the "simple event view" and all day indication. - * Use Attendees.ATTENDEE_STATUS_NONE for no response. - * @param selectedMillis The time to specify as selected - */ - public void sendEventRelatedEventWithExtra(Object sender, long eventType, long eventId, - long startMillis, long endMillis, int x, int y, long extraLong, long selectedMillis) { - sendEventRelatedEventWithExtraWithTitleWithCalendarId(sender, eventType, eventId, - startMillis, endMillis, x, y, extraLong, selectedMillis, null, -1); - } - - /** - * Helper for sending New/View/Edit/Delete events - * - * @param sender object of the caller - * @param eventType one of {@link EventType} - * @param eventId event id - * @param startMillis start time - * @param endMillis end time - * @param x x coordinate in the activity space - * @param y y coordinate in the activity space - * @param extraLong default response value for the "simple event view" and all day indication. - * Use Attendees.ATTENDEE_STATUS_NONE for no response. - * @param selectedMillis The time to specify as selected - * @param title The title of the event - * @param calendarId The id of the calendar which the event belongs to - */ - public void sendEventRelatedEventWithExtraWithTitleWithCalendarId(Object sender, long eventType, - long eventId, long startMillis, long endMillis, int x, int y, long extraLong, - long selectedMillis, String title, long calendarId) { - EventInfo info = new EventInfo(); - info.eventType = eventType; - if (eventType == EventType.VIEW_EVENT_DETAILS) { - info.viewType = ViewType.CURRENT; - } - - info.id = eventId; - info.startTime = new Time(Utils.getTimeZone(mContext, mUpdateTimezone)); - info.startTime.set(startMillis); - if (selectedMillis != -1) { - info.selectedTime = new Time(Utils.getTimeZone(mContext, mUpdateTimezone)); - info.selectedTime.set(selectedMillis); - } else { - info.selectedTime = info.startTime; - } - info.endTime = new Time(Utils.getTimeZone(mContext, mUpdateTimezone)); - info.endTime.set(endMillis); - info.x = x; - info.y = y; - info.extraLong = extraLong; - info.eventTitle = title; - info.calendarId = calendarId; - this.sendEvent(sender, info); - } - /** - * Helper for sending non-calendar-event events - * - * @param sender object of the caller - * @param eventType one of {@link EventType} - * @param start start time - * @param end end time - * @param eventId event id - * @param viewType {@link ViewType} - */ - public void sendEvent(Object sender, long eventType, Time start, Time end, long eventId, - int viewType) { - sendEvent(sender, eventType, start, end, start, eventId, viewType, EXTRA_GOTO_TIME, null, - null); - } - - /** - * sendEvent() variant with extraLong, search query, and search component name. - */ - public void sendEvent(Object sender, long eventType, Time start, Time end, long eventId, - int viewType, long extraLong, String query, ComponentName componentName) { - sendEvent(sender, eventType, start, end, start, eventId, viewType, extraLong, query, - componentName); - } - - public void sendEvent(Object sender, long eventType, Time start, Time end, Time selected, - long eventId, int viewType, long extraLong, String query, ComponentName componentName) { - EventInfo info = new EventInfo(); - info.eventType = eventType; - info.startTime = start; - info.selectedTime = selected; - info.endTime = end; - info.id = eventId; - info.viewType = viewType; - info.query = query; - info.componentName = componentName; - info.extraLong = extraLong; - this.sendEvent(sender, info); - } - - public void sendEvent(Object sender, final EventInfo event) { - // TODO Throw exception on invalid events - - if (DEBUG) { - Log.d(TAG, eventInfoToString(event)); - } - - Long filteredTypes = filters.get(sender); - if (filteredTypes != null && (filteredTypes.longValue() & event.eventType) != 0) { - // Suppress event per filter - if (DEBUG) { - Log.d(TAG, "Event suppressed"); - } - return; - } - - mPreviousViewType = mViewType; - - // Fix up view if not specified - if (event.viewType == ViewType.DETAIL) { - event.viewType = mDetailViewType; - mViewType = mDetailViewType; - } else if (event.viewType == ViewType.CURRENT) { - event.viewType = mViewType; - } else if (event.viewType != ViewType.EDIT) { - mViewType = event.viewType; - - if (event.viewType == ViewType.AGENDA || event.viewType == ViewType.DAY - || (Utils.getAllowWeekForDetailView() && event.viewType == ViewType.WEEK)) { - mDetailViewType = mViewType; - } - } - - if (DEBUG) { - Log.d(TAG, "vvvvvvvvvvvvvvv"); - Log.d(TAG, "Start " + (event.startTime == null ? "null" : event.startTime.toString())); - Log.d(TAG, "End " + (event.endTime == null ? "null" : event.endTime.toString())); - Log.d(TAG, "Select " + (event.selectedTime == null ? "null" : event.selectedTime.toString())); - Log.d(TAG, "mTime " + (mTime == null ? "null" : mTime.toString())); - } - - long startMillis = 0; - if (event.startTime != null) { - startMillis = event.startTime.toMillis(false); - } - - // Set mTime if selectedTime is set - if (event.selectedTime != null && event.selectedTime.toMillis(false) != 0) { - mTime.set(event.selectedTime); - } else { - if (startMillis != 0) { - // selectedTime is not set so set mTime to startTime iff it is not - // within start and end times - long mtimeMillis = mTime.toMillis(false); - if (mtimeMillis < startMillis - || (event.endTime != null && mtimeMillis > event.endTime.toMillis(false))) { - mTime.set(event.startTime); - } - } - event.selectedTime = mTime; - } - // Store the formatting flags if this is an update to the title - if (event.eventType == EventType.UPDATE_TITLE) { - mDateFlags = event.extraLong; - } - - // Fix up start time if not specified - if (startMillis == 0) { - event.startTime = mTime; - } - if (DEBUG) { - Log.d(TAG, "Start " + (event.startTime == null ? "null" : event.startTime.toString())); - Log.d(TAG, "End " + (event.endTime == null ? "null" : event.endTime.toString())); - Log.d(TAG, "Select " + (event.selectedTime == null ? "null" : event.selectedTime.toString())); - Log.d(TAG, "mTime " + (mTime == null ? "null" : mTime.toString())); - Log.d(TAG, "^^^^^^^^^^^^^^^"); - } - - // Store the eventId if we're entering edit event - if ((event.eventType - & (EventType.VIEW_EVENT_DETAILS)) - != 0) { - if (event.id > 0) { - mEventId = event.id; - } else { - mEventId = -1; - } - } - - boolean handled = false; - synchronized (this) { - mDispatchInProgressCounter ++; - - if (DEBUG) { - Log.d(TAG, "sendEvent: Dispatching to " + eventHandlers.size() + " handlers"); - } - // Dispatch to event handler(s) - if (mFirstEventHandler != null) { - // Handle the 'first' one before handling the others - EventHandler handler = mFirstEventHandler.second; - if (handler != null && (handler.getSupportedEventTypes() & event.eventType) != 0 - && !mToBeRemovedEventHandlers.contains(mFirstEventHandler.first)) { - handler.handleEvent(event); - handled = true; - } - } - for (Iterator<Entry<Integer, EventHandler>> handlers = - eventHandlers.entrySet().iterator(); handlers.hasNext();) { - Entry<Integer, EventHandler> entry = handlers.next(); - int key = entry.getKey(); - if (mFirstEventHandler != null && key == mFirstEventHandler.first) { - // If this was the 'first' handler it was already handled - continue; - } - EventHandler eventHandler = entry.getValue(); - if (eventHandler != null - && (eventHandler.getSupportedEventTypes() & event.eventType) != 0) { - if (mToBeRemovedEventHandlers.contains(key)) { - continue; - } - eventHandler.handleEvent(event); - handled = true; - } - } - - mDispatchInProgressCounter --; - - if (mDispatchInProgressCounter == 0) { - - // Deregister removed handlers - if (mToBeRemovedEventHandlers.size() > 0) { - for (Integer zombie : mToBeRemovedEventHandlers) { - eventHandlers.remove(zombie); - if (mFirstEventHandler != null && zombie.equals(mFirstEventHandler.first)) { - mFirstEventHandler = null; - } - } - mToBeRemovedEventHandlers.clear(); - } - // Add new handlers - if (mToBeAddedFirstEventHandler != null) { - mFirstEventHandler = mToBeAddedFirstEventHandler; - mToBeAddedFirstEventHandler = null; - } - if (mToBeAddedEventHandlers.size() > 0) { - for (Entry<Integer, EventHandler> food : mToBeAddedEventHandlers.entrySet()) { - eventHandlers.put(food.getKey(), food.getValue()); - } - } - } - } - } - - /** - * Adds or updates an event handler. This uses a LinkedHashMap so that we can - * replace fragments based on the view id they are being expanded into. - * - * @param key The view id or placeholder for this handler - * @param eventHandler Typically a fragment or activity in the calendar app - */ - public void registerEventHandler(int key, EventHandler eventHandler) { - synchronized (this) { - if (mDispatchInProgressCounter > 0) { - mToBeAddedEventHandlers.put(key, eventHandler); - } else { - eventHandlers.put(key, eventHandler); - } - } - } - - public void registerFirstEventHandler(int key, EventHandler eventHandler) { - synchronized (this) { - registerEventHandler(key, eventHandler); - if (mDispatchInProgressCounter > 0) { - mToBeAddedFirstEventHandler = new Pair<Integer, EventHandler>(key, eventHandler); - } else { - mFirstEventHandler = new Pair<Integer, EventHandler>(key, eventHandler); - } - } - } - - public void deregisterEventHandler(Integer key) { - synchronized (this) { - if (mDispatchInProgressCounter > 0) { - // To avoid ConcurrencyException, stash away the event handler for now. - mToBeRemovedEventHandlers.add(key); - } else { - eventHandlers.remove(key); - if (mFirstEventHandler != null && mFirstEventHandler.first == key) { - mFirstEventHandler = null; - } - } - } - } - - public void deregisterAllEventHandlers() { - synchronized (this) { - if (mDispatchInProgressCounter > 0) { - // To avoid ConcurrencyException, stash away the event handler for now. - mToBeRemovedEventHandlers.addAll(eventHandlers.keySet()); - } else { - eventHandlers.clear(); - mFirstEventHandler = null; - } - } - } - - // FRAG_TODO doesn't work yet - public void filterBroadcasts(Object sender, long eventTypes) { - filters.put(sender, eventTypes); - } - - /** - * @return the time that this controller is currently pointed at - */ - public long getTime() { - return mTime.toMillis(false); - } - - /** - * @return the last set of date flags sent with - * {@link EventType#UPDATE_TITLE} - */ - public long getDateFlags() { - return mDateFlags; - } - - /** - * Set the time this controller is currently pointed at - * - * @param millisTime Time since epoch in millis - */ - public void setTime(long millisTime) { - mTime.set(millisTime); - } - - /** - * @return the last event ID the edit view was launched with - */ - public long getEventId() { - return mEventId; - } - - public int getViewType() { - return mViewType; - } - - public int getPreviousViewType() { - return mPreviousViewType; - } - - public void launchViewEvent(long eventId, long startMillis, long endMillis, int response) { - Intent intent = new Intent(Intent.ACTION_VIEW); - Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); - intent.setData(eventUri); - intent.setClass(mContext, AllInOneActivity.class); - intent.putExtra(EXTRA_EVENT_BEGIN_TIME, startMillis); - intent.putExtra(EXTRA_EVENT_END_TIME, endMillis); - intent.putExtra(ATTENDEE_STATUS, response); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - mContext.startActivity(intent); - } - - // Forces the viewType. Should only be used for initialization. - public void setViewType(int viewType) { - mViewType = viewType; - } - - // Sets the eventId. Should only be used for initialization. - public void setEventId(long eventId) { - mEventId = eventId; - } - - private String eventInfoToString(EventInfo eventInfo) { - String tmp = "Unknown"; - - StringBuilder builder = new StringBuilder(); - if ((eventInfo.eventType & EventType.GO_TO) != 0) { - tmp = "Go to time/event"; - } else if ((eventInfo.eventType & EventType.VIEW_EVENT) != 0) { - tmp = "View event"; - } else if ((eventInfo.eventType & EventType.VIEW_EVENT_DETAILS) != 0) { - tmp = "View details"; - } else if ((eventInfo.eventType & EventType.EVENTS_CHANGED) != 0) { - tmp = "Refresh events"; - } else if ((eventInfo.eventType & EventType.USER_HOME) != 0) { - tmp = "Gone home"; - } else if ((eventInfo.eventType & EventType.UPDATE_TITLE) != 0) { - tmp = "Update title"; - } - builder.append(tmp); - builder.append(": id="); - builder.append(eventInfo.id); - builder.append(", selected="); - builder.append(eventInfo.selectedTime); - builder.append(", start="); - builder.append(eventInfo.startTime); - builder.append(", end="); - builder.append(eventInfo.endTime); - builder.append(", viewType="); - builder.append(eventInfo.viewType); - builder.append(", x="); - builder.append(eventInfo.x); - builder.append(", y="); - builder.append(eventInfo.y); - return builder.toString(); - } -} diff --git a/src/com/android/calendar/CalendarController.kt b/src/com/android/calendar/CalendarController.kt new file mode 100644 index 00000000..16ee8fdd --- /dev/null +++ b/src/com/android/calendar/CalendarController.kt @@ -0,0 +1,743 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME +import android.provider.CalendarContract.EXTRA_EVENT_END_TIME +import android.provider.CalendarContract.Attendees.ATTENDEE_STATUS +import android.content.ComponentName +import android.content.ContentUris +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Events +import android.text.format.Time +import android.util.Log +import android.util.Pair +import java.lang.ref.WeakReference +import java.util.LinkedHashMap +import java.util.LinkedList +import java.util.WeakHashMap + +class CalendarController private constructor(context: Context?) { + private var mContext: Context? = null + + // This uses a LinkedHashMap so that we can replace fragments based on the + // view id they are being expanded into since we can't guarantee a reference + // to the handler will be findable + private val eventHandlers: LinkedHashMap<Int, EventHandler> = + LinkedHashMap<Int, EventHandler>(5) + private val mToBeRemovedEventHandlers: LinkedList<Int> = LinkedList<Int>() + private val mToBeAddedEventHandlers: LinkedHashMap<Int, EventHandler> = + LinkedHashMap<Int, EventHandler>() + private var mFirstEventHandler: Pair<Int, EventHandler>? = null + private var mToBeAddedFirstEventHandler: Pair<Int, EventHandler>? = null + + @Volatile + private var mDispatchInProgressCounter = 0 + private val filters: WeakHashMap<Object, Long> = WeakHashMap<Object, Long>(1) + + // Forces the viewType. Should only be used for initialization. + var viewType = -1 + private var mDetailViewType = -1 + var previousViewType = -1 + private set + + // The last event ID the edit view was launched with + var eventId: Long = -1 + private val mTime: Time? = Time() + + // The last set of date flags sent with + var dateFlags: Long = 0 + private set + private val mUpdateTimezone: Runnable = object : Runnable { + @Override + override fun run() { + mTime?.switchTimezone(Utils.getTimeZone(mContext, this)) + } + } + + /** + * One of the event types that are sent to or from the controller + */ + interface EventType { + companion object { + // Simple view of an event + const val VIEW_EVENT = 1L shl 1 + + // Full detail view in read only mode + const val VIEW_EVENT_DETAILS = 1L shl 2 + + // full detail view in edit mode + const val EDIT_EVENT = 1L shl 3 + const val GO_TO = 1L shl 5 + const val EVENTS_CHANGED = 1L shl 7 + const val USER_HOME = 1L shl 9 + + // date range has changed, update the title + const val UPDATE_TITLE = 1L shl 10 + } + } + + /** + * One of the Agenda/Day/Week/Month view types + */ + interface ViewType { + companion object { + const val DETAIL = -1 + const val CURRENT = 0 + const val AGENDA = 1 + const val DAY = 2 + const val WEEK = 3 + const val MONTH = 4 + const val EDIT = 5 + const val MAX_VALUE = 5 + } + } + + class EventInfo { + @JvmField var eventType: Long = 0 // one of the EventType + @JvmField var viewType = 0 // one of the ViewType + @JvmField var id: Long = 0 // event id + @JvmField var selectedTime: Time? = null // the selected time in focus + + // Event start and end times. All-day events are represented in: + // - local time for GO_TO commands + // - UTC time for VIEW_EVENT and other event-related commands + @JvmField var startTime: Time? = null + @JvmField var endTime: Time? = null + @JvmField var x = 0 // x coordinate in the activity space + @JvmField var y = 0 // y coordinate in the activity space + @JvmField var query: String? = null // query for a user search + @JvmField var componentName: ComponentName? = null // used in combination with query + @JvmField var eventTitle: String? = null + @JvmField var calendarId: Long = 0 + + /** + * For EventType.VIEW_EVENT: + * It is the default attendee response and an all day event indicator. + * Set to Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED, + * Attendees.ATTENDEE_STATUS_DECLINED, or Attendees.ATTENDEE_STATUS_TENTATIVE. + * To signal the event is an all-day event, "or" ALL_DAY_MASK with the response. + * Alternatively, use buildViewExtraLong(), getResponse(), and isAllDay(). + * + * + * For EventType.GO_TO: + * Set to [.EXTRA_GOTO_TIME] to go to the specified date/time. + * Set to [.EXTRA_GOTO_DATE] to consider the date but ignore the time. + * Set to [.EXTRA_GOTO_BACK_TO_PREVIOUS] if back should bring back previous view. + * Set to [.EXTRA_GOTO_TODAY] if this is a user request to go to the current time. + * + * + * For EventType.UPDATE_TITLE: + * Set formatting flags for Utils.formatDateRange + */ + @JvmField var extraLong: Long = 0 + val isAllDay: Boolean + get() { + if (eventType != EventType.VIEW_EVENT) { + Log.wtf(TAG, "illegal call to isAllDay , wrong event type $eventType") + return false + } + return if (extraLong and ALL_DAY_MASK != 0L) true else false + } + val response: Int + get() { + if (eventType != EventType.VIEW_EVENT) { + Log.wtf(TAG, "illegal call to getResponse , wrong event type $eventType") + return Attendees.ATTENDEE_STATUS_NONE + } + val response = (extraLong and ATTENTEE_STATUS_MASK).toInt() + when (response) { + ATTENDEE_STATUS_NONE_MASK -> return Attendees.ATTENDEE_STATUS_NONE + ATTENDEE_STATUS_ACCEPTED_MASK -> return Attendees.ATTENDEE_STATUS_ACCEPTED + ATTENDEE_STATUS_DECLINED_MASK -> return Attendees.ATTENDEE_STATUS_DECLINED + ATTENDEE_STATUS_TENTATIVE_MASK -> return Attendees.ATTENDEE_STATUS_TENTATIVE + else -> Log.wtf(TAG, "Unknown attendee response $response") + } + return ATTENDEE_STATUS_NONE_MASK + } + + companion object { + private const val ATTENTEE_STATUS_MASK: Long = 0xFF + private const val ALL_DAY_MASK: Long = 0x100 + private const val ATTENDEE_STATUS_NONE_MASK = 0x01 + private const val ATTENDEE_STATUS_ACCEPTED_MASK = 0x02 + private const val ATTENDEE_STATUS_DECLINED_MASK = 0x04 + private const val ATTENDEE_STATUS_TENTATIVE_MASK = 0x08 + + // Used to build the extra long for a VIEW event. + @JvmStatic fun buildViewExtraLong(response: Int, allDay: Boolean): Long { + var extra = if (allDay) ALL_DAY_MASK else 0 + extra = when (response) { + Attendees.ATTENDEE_STATUS_NONE -> extra or + ATTENDEE_STATUS_NONE_MASK.toLong() + Attendees.ATTENDEE_STATUS_ACCEPTED -> extra or + ATTENDEE_STATUS_ACCEPTED_MASK.toLong() + Attendees.ATTENDEE_STATUS_DECLINED -> extra or + ATTENDEE_STATUS_DECLINED_MASK.toLong() + Attendees.ATTENDEE_STATUS_TENTATIVE -> extra or + ATTENDEE_STATUS_TENTATIVE_MASK.toLong() + else -> { + Log.wtf( + TAG, + "Unknown attendee response $response" + ) + extra or ATTENDEE_STATUS_NONE_MASK.toLong() + } + } + return extra + } + } + } + + interface EventHandler { + val supportedEventTypes: Long + fun handleEvent(event: EventInfo?) + + /** + * This notifies the handler that the database has changed and it should + * update its view. + */ + fun eventsChanged() + } + + fun sendEventRelatedEvent( + sender: Object?, + eventType: Long, + eventId: Long, + startMillis: Long, + endMillis: Long, + x: Int, + y: Int, + selectedMillis: Long + ) { + // TODO: pass the real allDay status or at least a status that says we don't know the + // status and have the receiver query the data. + // The current use of this method for VIEW_EVENT is by the day view to show an EventInfo + // so currently the missing allDay status has no effect. + sendEventRelatedEventWithExtra( + sender, eventType, eventId, startMillis, endMillis, x, y, + EventInfo.buildViewExtraLong(Attendees.ATTENDEE_STATUS_NONE, false), + selectedMillis + ) + } + + /** + * Helper for sending New/View/Edit/Delete events + * + * @param sender object of the caller + * @param eventType one of [EventType] + * @param eventId event id + * @param startMillis start time + * @param endMillis end time + * @param x x coordinate in the activity space + * @param y y coordinate in the activity space + * @param extraLong default response value for the "simple event view" and all day indication. + * Use Attendees.ATTENDEE_STATUS_NONE for no response. + * @param selectedMillis The time to specify as selected + */ + fun sendEventRelatedEventWithExtra( + sender: Object?, + eventType: Long, + eventId: Long, + startMillis: Long, + endMillis: Long, + x: Int, + y: Int, + extraLong: Long, + selectedMillis: Long + ) { + sendEventRelatedEventWithExtraWithTitleWithCalendarId( + sender, eventType, eventId, + startMillis, endMillis, x, y, extraLong, selectedMillis, null, -1 + ) + } + + /** + * Helper for sending New/View/Edit/Delete events + * + * @param sender object of the caller + * @param eventType one of [EventType] + * @param eventId event id + * @param startMillis start time + * @param endMillis end time + * @param x x coordinate in the activity space + * @param y y coordinate in the activity space + * @param extraLong default response value for the "simple event view" and all day indication. + * Use Attendees.ATTENDEE_STATUS_NONE for no response. + * @param selectedMillis The time to specify as selected + * @param title The title of the event + * @param calendarId The id of the calendar which the event belongs to + */ + fun sendEventRelatedEventWithExtraWithTitleWithCalendarId( + sender: Object?, + eventType: Long, + eventId: Long, + startMillis: Long, + endMillis: Long, + x: Int, + y: Int, + extraLong: Long, + selectedMillis: Long, + title: String?, + calendarId: Long + ) { + val info = EventInfo() + info.eventType = eventType + if (eventType == EventType.VIEW_EVENT_DETAILS) { + info.viewType = ViewType.CURRENT + } + info.id = eventId + info.startTime = Time(Utils.getTimeZone(mContext, mUpdateTimezone)) + (info.startTime as Time).set(startMillis) + if (selectedMillis != -1L) { + info.selectedTime = Time(Utils.getTimeZone(mContext, mUpdateTimezone)) + (info.selectedTime as Time).set(selectedMillis) + } else { + info.selectedTime = info.startTime + } + info.endTime = Time(Utils.getTimeZone(mContext, mUpdateTimezone)) + (info.endTime as Time).set(endMillis) + info.x = x + info.y = y + info.extraLong = extraLong + info.eventTitle = title + info.calendarId = calendarId + this.sendEvent(sender, info) + } + + /** + * Helper for sending non-calendar-event events + * + * @param sender object of the caller + * @param eventType one of [EventType] + * @param start start time + * @param end end time + * @param eventId event id + * @param viewType [ViewType] + */ + fun sendEvent( + sender: Object?, + eventType: Long, + start: Time?, + end: Time?, + eventId: Long, + viewType: Int + ) { + sendEvent( + sender, eventType, start, end, start, eventId, viewType, EXTRA_GOTO_TIME, null, + null + ) + } + + /** + * sendEvent() variant with extraLong, search query, and search component name. + */ + fun sendEvent( + sender: Object?, + eventType: Long, + start: Time?, + end: Time?, + eventId: Long, + viewType: Int, + extraLong: Long, + query: String?, + componentName: ComponentName? + ) { + sendEvent( + sender, eventType, start, end, start, eventId, viewType, extraLong, query, + componentName + ) + } + + fun sendEvent( + sender: Object?, + eventType: Long, + start: Time?, + end: Time?, + selected: Time?, + eventId: Long, + viewType: Int, + extraLong: Long, + query: String?, + componentName: ComponentName? + ) { + val info = EventInfo() + info.eventType = eventType + info.startTime = start + info.selectedTime = selected + info.endTime = end + info.id = eventId + info.viewType = viewType + info.query = query + info.componentName = componentName + info.extraLong = extraLong + this.sendEvent(sender, info) + } + + fun sendEvent(sender: Object?, event: EventInfo) { + // TODO Throw exception on invalid events + if (DEBUG) { + Log.d(TAG, eventInfoToString(event)) + } + val filteredTypes: Long? = filters.get(sender) + if (filteredTypes != null && filteredTypes.toLong() and event.eventType != 0L) { + // Suppress event per filter + if (DEBUG) { + Log.d(TAG, "Event suppressed") + } + return + } + previousViewType = viewType + + // Fix up view if not specified + if (event.viewType == ViewType.DETAIL) { + event.viewType = mDetailViewType + viewType = mDetailViewType + } else if (event.viewType == ViewType.CURRENT) { + event.viewType = viewType + } else if (event.viewType != ViewType.EDIT) { + viewType = event.viewType + if (event.viewType == ViewType.AGENDA || event.viewType == ViewType.DAY || + Utils.getAllowWeekForDetailView() && event.viewType == ViewType.WEEK) { + mDetailViewType = viewType + } + } + if (DEBUG) { + Log.d(TAG, "vvvvvvvvvvvvvvv") + Log.d( + TAG, + "Start " + if (event.startTime == null) "null" else event.startTime.toString() + ) + Log.d(TAG, "End " + if (event.endTime == null) "null" else event.endTime.toString()) + Log.d( + TAG, + "Select " + if (event.selectedTime == null) "null" + else event.selectedTime.toString() + ) + Log.d(TAG, "mTime " + if (mTime == null) "null" else mTime.toString()) + } + var startMillis: Long = 0 + val temp = event.startTime + if (temp != null) { + startMillis = (event.startTime as Time).toMillis(false) + } + + // Set mTime if selectedTime is set + val temp1 = event.selectedTime + if (temp1 != null && temp1?.toMillis(false) != 0L) { + mTime?.set(event.selectedTime) + } else { + if (startMillis != 0L) { + // selectedTime is not set so set mTime to startTime iff it is not + // within start and end times + val mtimeMillis: Long = mTime?.toMillis(false) as Long + val temp2 = event.endTime + if (mtimeMillis < startMillis || + temp2 != null && mtimeMillis > temp2.toMillis(false)) { + mTime?.set(event.startTime) + } + } + event.selectedTime = mTime + } + // Store the formatting flags if this is an update to the title + if (event.eventType == EventType.UPDATE_TITLE) { + dateFlags = event.extraLong + } + + // Fix up start time if not specified + if (startMillis == 0L) { + event.startTime = mTime + } + if (DEBUG) { + Log.d( + TAG, + "Start " + if (event.startTime == null) "null" else + event.startTime.toString() + ) + Log.d(TAG, "End " + if (event.endTime == null) "null" else + event.endTime.toString()) + Log.d( + TAG, + "Select " + if (event.selectedTime == null) "null" else + event.selectedTime.toString() + ) + Log.d(TAG, "mTime " + if (mTime == null) "null" else mTime.toString()) + Log.d(TAG, "^^^^^^^^^^^^^^^") + } + + // Store the eventId if we're entering edit event + if ((event.eventType and EventType.VIEW_EVENT_DETAILS) != 0L) { + if (event.id > 0) { + eventId = event.id + } else { + eventId = -1 + } + } + var handled = false + synchronized(this) { + mDispatchInProgressCounter++ + if (DEBUG) { + Log.d( + TAG, + "sendEvent: Dispatching to " + eventHandlers.size.toString() + " handlers" + ) + } + // Dispatch to event handler(s) + val temp3 = mFirstEventHandler + if (temp3 != null) { + // Handle the 'first' one before handling the others + val handler: EventHandler? = mFirstEventHandler?.second + if (handler != null && handler.supportedEventTypes and event.eventType != 0L && + !mToBeRemovedEventHandlers.contains(mFirstEventHandler?.first)) { + handler.handleEvent(event) + handled = true + } + } + val handlers: MutableIterator<MutableMap.MutableEntry<Int, + CalendarController.EventHandler>> = eventHandlers.entries.iterator() + while (handlers.hasNext()) { + val entry: MutableMap.MutableEntry<Int, + CalendarController.EventHandler> = handlers.next() + val key: Int = entry.key.toInt() + val temp4 = mFirstEventHandler + if (temp4 != null && key.toInt() == temp4.first.toInt()) { + // If this was the 'first' handler it was already handled + continue + } + val eventHandler: EventHandler = entry.value + if (eventHandler != null && + eventHandler.supportedEventTypes and event.eventType != 0L) { + if (mToBeRemovedEventHandlers.contains(key)) { + continue + } + eventHandler.handleEvent(event) + handled = true + } + } + mDispatchInProgressCounter-- + if (mDispatchInProgressCounter == 0) { + + // Deregister removed handlers + if (mToBeRemovedEventHandlers.size > 0) { + for (zombie in mToBeRemovedEventHandlers) { + eventHandlers.remove(zombie) + val temp5 = mFirstEventHandler + if (temp5 != null && zombie.equals(temp5.first)) { + mFirstEventHandler = null + } + } + mToBeRemovedEventHandlers.clear() + } + // Add new handlers + if (mToBeAddedFirstEventHandler != null) { + mFirstEventHandler = mToBeAddedFirstEventHandler + mToBeAddedFirstEventHandler = null + } + if (mToBeAddedEventHandlers.size > 0) { + for (food in mToBeAddedEventHandlers.entries) { + eventHandlers.put(food.key, food.value) + } + } + } + } + } + + /** + * Adds or updates an event handler. This uses a LinkedHashMap so that we can + * replace fragments based on the view id they are being expanded into. + * + * @param key The view id or placeholder for this handler + * @param eventHandler Typically a fragment or activity in the calendar app + */ + fun registerEventHandler(key: Int, eventHandler: EventHandler?) { + synchronized(this) { + if (mDispatchInProgressCounter > 0) { + mToBeAddedEventHandlers.put(key, + eventHandler as CalendarController.EventHandler) + } else { + eventHandlers.put(key, eventHandler as CalendarController.EventHandler) + } + } + } + + fun registerFirstEventHandler(key: Int, eventHandler: EventHandler?) { + synchronized(this) { + registerEventHandler(key, eventHandler) + if (mDispatchInProgressCounter > 0) { + mToBeAddedFirstEventHandler = Pair<Int, EventHandler>(key, eventHandler) + } else { + mFirstEventHandler = Pair<Int, EventHandler>(key, eventHandler) + } + } + } + + fun deregisterEventHandler(key: Int) { + synchronized(this) { + if (mDispatchInProgressCounter > 0) { + // To avoid ConcurrencyException, stash away the event handler for now. + mToBeRemovedEventHandlers.add(key) + } else { + eventHandlers.remove(key) + val temp6 = mFirstEventHandler + if (temp6 != null && temp6.first == key) { + mFirstEventHandler = null + } else {} + } + } + } + + fun deregisterAllEventHandlers() { + synchronized(this) { + if (mDispatchInProgressCounter > 0) { + // To avoid ConcurrencyException, stash away the event handler for now. + mToBeRemovedEventHandlers.addAll(eventHandlers.keys) + } else { + eventHandlers.clear() + mFirstEventHandler = null + } + } + } + + // FRAG_TODO doesn't work yet + fun filterBroadcasts(sender: Object?, eventTypes: Long) { + filters.put(sender, eventTypes) + } + /** + * @return the time that this controller is currently pointed at + */ + /** + * Set the time this controller is currently pointed at + * + * @param millisTime Time since epoch in millis + */ + var time: Long? + get() = mTime?.toMillis(false) + set(millisTime) { + mTime?.set(millisTime as Long) + } + + fun launchViewEvent(eventId: Long, startMillis: Long, endMillis: Long, response: Int) { + val intent = Intent(Intent.ACTION_VIEW) + val eventUri: Uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId) + intent.setData(eventUri) + intent.setClass(mContext as Context, AllInOneActivity::class.java) + intent.putExtra(EXTRA_EVENT_BEGIN_TIME, startMillis) + intent.putExtra(EXTRA_EVENT_END_TIME, endMillis) + intent.putExtra(ATTENDEE_STATUS, response) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + mContext?.startActivity(intent) + } + + private fun eventInfoToString(eventInfo: EventInfo): String { + var tmp = "Unknown" + val builder = StringBuilder() + if (eventInfo.eventType and EventType.GO_TO != 0L) { + tmp = "Go to time/event" + } else if (eventInfo.eventType and EventType.VIEW_EVENT != 0L) { + tmp = "View event" + } else if (eventInfo.eventType and EventType.VIEW_EVENT_DETAILS != 0L) { + tmp = "View details" + } else if (eventInfo.eventType and EventType.EVENTS_CHANGED != 0L) { + tmp = "Refresh events" + } else if (eventInfo.eventType and EventType.USER_HOME != 0L) { + tmp = "Gone home" + } else if (eventInfo.eventType and EventType.UPDATE_TITLE != 0L) { + tmp = "Update title" + } + builder.append(tmp) + builder.append(": id=") + builder.append(eventInfo.id) + builder.append(", selected=") + builder.append(eventInfo.selectedTime) + builder.append(", start=") + builder.append(eventInfo.startTime) + builder.append(", end=") + builder.append(eventInfo.endTime) + builder.append(", viewType=") + builder.append(eventInfo.viewType) + builder.append(", x=") + builder.append(eventInfo.x) + builder.append(", y=") + builder.append(eventInfo.y) + return builder.toString() + } + + companion object { + private const val DEBUG = false + private const val TAG = "CalendarController" + const val EVENT_EDIT_ON_LAUNCH = "editMode" + const val MIN_CALENDAR_YEAR = 1970 + const val MAX_CALENDAR_YEAR = 2036 + const val MIN_CALENDAR_WEEK = 0 + const val MAX_CALENDAR_WEEK = 3497 // weeks between 1/1/1970 and 1/1/2037 + private val instances: WeakHashMap<Context, WeakReference<CalendarController>> = + WeakHashMap<Context, WeakReference<CalendarController>>() + + /** + * Pass to the ExtraLong parameter for EventType.GO_TO to signal the time + * can be ignored + */ + const val EXTRA_GOTO_DATE: Long = 1 + const val EXTRA_GOTO_TIME: Long = 2 + const val EXTRA_GOTO_BACK_TO_PREVIOUS: Long = 4 + const val EXTRA_GOTO_TODAY: Long = 8 + + /** + * Creates and/or returns an instance of CalendarController associated with + * the supplied context. It is best to pass in the current Activity. + * + * @param context The activity if at all possible. + */ + @JvmStatic fun getInstance(context: Context?): CalendarController? { + synchronized(instances) { + var controller: CalendarController? = null + val weakController: WeakReference<CalendarController>? = instances.get(context) + if (weakController != null) { + controller = weakController.get() + } + if (controller == null) { + controller = CalendarController(context) + instances.put(context, WeakReference(controller)) + } + return controller + } + } + + /** + * Removes an instance when it is no longer needed. This should be called in + * an activity's onDestroy method. + * + * @param context The activity used to create the controller + */ + @JvmStatic fun removeInstance(context: Context?) { + instances.remove(context) + } + } + + init { + mContext = context + mUpdateTimezone.run() + mTime?.setToNow() + mDetailViewType = Utils.getSharedPreference( + mContext, + GeneralPreferences.KEY_DETAILED_VIEW, + GeneralPreferences.DEFAULT_DETAILED_VIEW + ) + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/CalendarData.java b/src/com/android/calendar/CalendarData.kt index 5c8456fa..7370f2e2 100644 --- a/src/com/android/calendar/CalendarData.java +++ b/src/com/android/calendar/CalendarData.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006 The Android Open Source Project + * Copyright (C) 2021 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. @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.calendar -package com.android.calendar; +object CalendarData { + @JvmField + val s12HoursNoAmPm = arrayOf("12", "1", "2", "3", "4", + "5", "6", "7", "8", "9", "10", "11", "12", + "1", "2", "3", "4", "5", "6", "7", "8", + "9", "10", "11", "12") -public final class CalendarData { - static final String[] s12HoursNoAmPm = { "12", "1", "2", "3", "4", - "5", "6", "7", "8", "9", "10", "11", "12", - "1", "2", "3", "4", "5", "6", "7", "8", - "9", "10", "11", "12" }; - - static final String[] s24Hours = { "00", "01", "02", "03", "04", "05", - "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", - "17", "18", "19", "20", "21", "22", "23", "00" }; -} + @JvmField + val s24Hours = arrayOf("00", "01", "02", "03", "04", "05", + "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", + "17", "18", "19", "20", "21", "22", "23", "00") +}
\ No newline at end of file diff --git a/src/com/android/calendar/CalendarUtils.java b/src/com/android/calendar/CalendarUtils.java deleted file mode 100644 index 0238c321..00000000 --- a/src/com/android/calendar/CalendarUtils.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar; - -import android.content.AsyncQueryHandler; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.os.Looper; -import android.provider.CalendarContract.CalendarCache; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; - -import java.util.Formatter; -import java.util.HashSet; -import java.util.Locale; - -/** - * A class containing utility methods related to Calendar apps. - * - * This class is expected to move into the app framework eventually. - */ -public class CalendarUtils { - private static final boolean DEBUG = false; - private static final String TAG = "CalendarUtils"; - - /** - * This class contains methods specific to reading and writing time zone - * values. - */ - public static class TimeZoneUtils { - private static final String[] TIMEZONE_TYPE_ARGS = { CalendarCache.KEY_TIMEZONE_TYPE }; - private static final String[] TIMEZONE_INSTANCES_ARGS = - { CalendarCache.KEY_TIMEZONE_INSTANCES }; - public static final String[] CALENDAR_CACHE_POJECTION = { - CalendarCache.KEY, CalendarCache.VALUE - }; - - private static StringBuilder mSB = new StringBuilder(50); - private static Formatter mF = new Formatter(mSB, Locale.getDefault()); - private volatile static boolean mFirstTZRequest = true; - private volatile static boolean mTZQueryInProgress = false; - - private volatile static boolean mUseHomeTZ = false; - private volatile static String mHomeTZ = Time.getCurrentTimezone(); - - private static HashSet<Runnable> mTZCallbacks = new HashSet<Runnable>(); - private static int mToken = 1; - private static AsyncTZHandler mHandler; - - // The name of the shared preferences file. This name must be maintained for historical - // reasons, as it's what PreferenceManager assigned the first time the file was created. - private final String mPrefsName; - - /** - * This is the key used for writing whether or not a home time zone should - * be used in the Calendar app to the Calendar Preferences. - */ - public static final String KEY_HOME_TZ_ENABLED = "preferences_home_tz_enabled"; - /** - * This is the key used for writing the time zone that should be used if - * home time zones are enabled for the Calendar app. - */ - public static final String KEY_HOME_TZ = "preferences_home_tz"; - - /** - * This is a helper class for handling the async queries and updates for the - * time zone settings in Calendar. - */ - private class AsyncTZHandler extends AsyncQueryHandler { - public AsyncTZHandler(ContentResolver cr) { - super(cr); - } - - @Override - protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - synchronized (mTZCallbacks) { - if (cursor == null) { - mTZQueryInProgress = false; - mFirstTZRequest = true; - return; - } - - boolean writePrefs = false; - // Check the values in the db - int keyColumn = cursor.getColumnIndexOrThrow(CalendarCache.KEY); - int valueColumn = cursor.getColumnIndexOrThrow(CalendarCache.VALUE); - while(cursor.moveToNext()) { - String key = cursor.getString(keyColumn); - String value = cursor.getString(valueColumn); - if (TextUtils.equals(key, CalendarCache.KEY_TIMEZONE_TYPE)) { - boolean useHomeTZ = !TextUtils.equals( - value, CalendarCache.TIMEZONE_TYPE_AUTO); - if (useHomeTZ != mUseHomeTZ) { - writePrefs = true; - mUseHomeTZ = useHomeTZ; - } - } else if (TextUtils.equals( - key, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)) { - if (!TextUtils.isEmpty(value) && !TextUtils.equals(mHomeTZ, value)) { - writePrefs = true; - mHomeTZ = value; - } - } - } - cursor.close(); - if (writePrefs) { - SharedPreferences prefs = getSharedPreferences((Context)cookie, mPrefsName); - // Write the prefs - setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ); - setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ); - } - - mTZQueryInProgress = false; - for (Runnable callback : mTZCallbacks) { - if (callback != null) { - callback.run(); - } - } - mTZCallbacks.clear(); - } - } - } - - /** - * The name of the file where the shared prefs for Calendar are stored - * must be provided. All activities within an app should provide the - * same preferences name or behavior may become erratic. - * - * @param prefsName - */ - public TimeZoneUtils(String prefsName) { - mPrefsName = prefsName; - } - - /** - * Formats a date or a time range according to the local conventions. - * - * This formats a date/time range using Calendar's time zone and the - * local conventions for the region of the device. - * - * If the {@link DateUtils#FORMAT_UTC} flag is used it will pass in - * the UTC time zone instead. - * - * @param context the context is required only if the time is shown - * @param startMillis the start time in UTC milliseconds - * @param endMillis the end time in UTC milliseconds - * @param flags a bit mask of options See - * {@link DateUtils#formatDateRange(Context, Formatter, long, long, int, String) formatDateRange} - * @return a string containing the formatted date/time range. - */ - public String formatDateRange(Context context, long startMillis, - long endMillis, int flags) { - String date; - String tz; - if ((flags & DateUtils.FORMAT_UTC) != 0) { - tz = Time.TIMEZONE_UTC; - } else { - tz = getTimeZone(context, null); - } - synchronized (mSB) { - mSB.setLength(0); - date = DateUtils.formatDateRange(context, mF, startMillis, endMillis, flags, - tz).toString(); - } - return date; - } - - /** - * Writes a new home time zone to the db. - * - * Updates the home time zone in the db asynchronously and updates - * the local cache. Sending a time zone of - * {@link CalendarCache#TIMEZONE_TYPE_AUTO} will cause it to be set - * to the device's time zone. null or empty tz will be ignored. - * - * @param context The calling activity - * @param timeZone The time zone to set Calendar to, or - * {@link CalendarCache#TIMEZONE_TYPE_AUTO} - */ - public void setTimeZone(Context context, String timeZone) { - if (TextUtils.isEmpty(timeZone)) { - if (DEBUG) { - Log.d(TAG, "Empty time zone, nothing to be done."); - } - return; - } - boolean updatePrefs = false; - synchronized (mTZCallbacks) { - if (CalendarCache.TIMEZONE_TYPE_AUTO.equals(timeZone)) { - if (mUseHomeTZ) { - updatePrefs = true; - } - mUseHomeTZ = false; - } else { - if (!mUseHomeTZ || !TextUtils.equals(mHomeTZ, timeZone)) { - updatePrefs = true; - } - mUseHomeTZ = true; - mHomeTZ = timeZone; - } - } - if (updatePrefs) { - // Write the prefs - SharedPreferences prefs = getSharedPreferences(context, mPrefsName); - setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ); - setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ); - - // Update the db - ContentValues values = new ContentValues(); - if (mHandler != null) { - mHandler.cancelOperation(mToken); - } - - mHandler = new AsyncTZHandler(context.getContentResolver()); - - // skip 0 so query can use it - if (++mToken == 0) { - mToken = 1; - } - - // Write the use home tz setting - values.put(CalendarCache.VALUE, mUseHomeTZ ? CalendarCache.TIMEZONE_TYPE_HOME - : CalendarCache.TIMEZONE_TYPE_AUTO); - mHandler.startUpdate(mToken, null, CalendarCache.URI, values, "key=?", - TIMEZONE_TYPE_ARGS); - - // If using a home tz write it to the db - if (mUseHomeTZ) { - ContentValues values2 = new ContentValues(); - values2.put(CalendarCache.VALUE, mHomeTZ); - mHandler.startUpdate(mToken, null, CalendarCache.URI, values2, - "key=?", TIMEZONE_INSTANCES_ARGS); - } - } - } - - /** - * Gets the time zone that Calendar should be displayed in - * - * This is a helper method to get the appropriate time zone for Calendar. If this - * is the first time this method has been called it will initiate an asynchronous - * query to verify that the data in preferences is correct. The callback supplied - * will only be called if this query returns a value other than what is stored in - * preferences and should cause the calling activity to refresh anything that - * depends on calling this method. - * - * @param context The calling activity - * @param callback The runnable that should execute if a query returns new values - * @return The string value representing the time zone Calendar should display - */ - public String getTimeZone(Context context, Runnable callback) { - synchronized (mTZCallbacks){ - if (mFirstTZRequest) { - SharedPreferences prefs = getSharedPreferences(context, mPrefsName); - mUseHomeTZ = prefs.getBoolean(KEY_HOME_TZ_ENABLED, false); - mHomeTZ = prefs.getString(KEY_HOME_TZ, Time.getCurrentTimezone()); - - // Only check content resolver if we have a looper to attach to use - if (Looper.myLooper() != null) { - mTZQueryInProgress = true; - mFirstTZRequest = false; - - // When the async query returns it should synchronize on - // mTZCallbacks, update mUseHomeTZ, mHomeTZ, and the - // preferences, set mTZQueryInProgress to false, and call all - // the runnables in mTZCallbacks. - if (mHandler == null) { - mHandler = new AsyncTZHandler(context.getContentResolver()); - } - mHandler.startQuery(0, context, CalendarCache.URI, CALENDAR_CACHE_POJECTION, - null, null, null); - } - } - if (mTZQueryInProgress) { - mTZCallbacks.add(callback); - } - } - return mUseHomeTZ ? mHomeTZ : Time.getCurrentTimezone(); - } - - /** - * Forces a query of the database to check for changes to the time zone. - * This should be called if another app may have modified the db. If a - * query is already in progress the callback will be added to the list - * of callbacks to be called when it returns. - * - * @param context The calling activity - * @param callback The runnable that should execute if a query returns - * new values - */ - public void forceDBRequery(Context context, Runnable callback) { - synchronized (mTZCallbacks){ - if (mTZQueryInProgress) { - mTZCallbacks.add(callback); - return; - } - mFirstTZRequest = true; - getTimeZone(context, callback); - } - } - } - - /** - * A helper method for writing a String value to the preferences - * asynchronously. - * - * @param context A context with access to the correct preferences - * @param key The preference to write to - * @param value The value to write - */ - public static void setSharedPreference(SharedPreferences prefs, String key, String value) { -// SharedPreferences prefs = getSharedPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(key, value); - editor.apply(); - } - - /** - * A helper method for writing a boolean value to the preferences - * asynchronously. - * - * @param context A context with access to the correct preferences - * @param key The preference to write to - * @param value The value to write - */ - public static void setSharedPreference(SharedPreferences prefs, String key, boolean value) { -// SharedPreferences prefs = getSharedPreferences(context, prefsName); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(key, value); - editor.apply(); - } - - /** Return a properly configured SharedPreferences instance */ - public static SharedPreferences getSharedPreferences(Context context, String prefsName) { - return context.getSharedPreferences(prefsName, Context.MODE_PRIVATE); - } -} diff --git a/src/com/android/calendar/CalendarUtils.kt b/src/com/android/calendar/CalendarUtils.kt new file mode 100644 index 00000000..94ca7234 --- /dev/null +++ b/src/com/android/calendar/CalendarUtils.kt @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.content.AsyncQueryHandler +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.content.SharedPreferences +import android.database.Cursor +import android.os.Looper +import android.provider.CalendarContract.CalendarCache +import android.text.TextUtils +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log + +import java.util.Formatter +import java.util.HashSet +import java.util.Locale + +/** + * A class containing utility methods related to Calendar apps. + * + * This class is expected to move into the app framework eventually. + */ +class CalendarUtils { + + companion object { + private const val DEBUG = false + private const val TAG = "CalendarUtils" + + /** + * A helper method for writing a boolean value to the preferences + * asynchronously. + * + * @param context A context with access to the correct preferences + * @param key The preference to write to + * @param value The value to write + */ + @JvmStatic + fun setSharedPreference(prefs: SharedPreferences, key: String?, value: Boolean) { + val editor: SharedPreferences.Editor = prefs.edit() + editor.putBoolean(key, value) + editor.apply() + } + + /** Return a properly configured SharedPreferences instance */ + @JvmStatic + fun getSharedPreferences(context: Context, prefsName: String?): SharedPreferences { + return context.getSharedPreferences(prefsName, Context.MODE_PRIVATE) + } + + /** + * A helper method for writing a String value to the preferences + * asynchronously. + * + * @param context A context with access to the correct preferences + * @param key The preference to write to + * @param value The value to write + */ + @JvmStatic + fun setSharedPreference(prefs: SharedPreferences, key: String?, value: String?) { + val editor: SharedPreferences.Editor = prefs.edit() + editor.putString(key, value) + editor.apply() + } + } + + /** + * This class contains methods specific to reading and writing time zone + * values. + */ + class TimeZoneUtils + /** + * The name of the file where the shared prefs for Calendar are stored + * must be provided. All activities within an app should provide the + * same preferences name or behavior may become erratic. + * + * @param prefsName + */( // The name of the shared preferences file. This name must be maintained for historical + // reasons, as it's what PreferenceManager assigned the first time the file was created. + private val mPrefsName: String) { + /** + * This is a helper class for handling the async queries and updates for the + * time zone settings in Calendar. + */ + private inner class AsyncTZHandler(cr: ContentResolver?) : AsyncQueryHandler(cr) { + protected override fun onQueryComplete(token: Int, cookie: Any?, cursor: Cursor?) { + synchronized(mTZCallbacks) { + if (cursor == null) { + mTZQueryInProgress = false + mFirstTZRequest = true + return + } + var writePrefs = false + // Check the values in the db + val keyColumn: Int = cursor.getColumnIndexOrThrow(CalendarCache.KEY) + val valueColumn: Int = cursor.getColumnIndexOrThrow(CalendarCache.VALUE) + while (cursor.moveToNext()) { + val key: String = cursor.getString(keyColumn) + val value: String = cursor.getString(valueColumn) + if (TextUtils.equals(key, CalendarCache.KEY_TIMEZONE_TYPE)) { + val useHomeTZ: Boolean = !TextUtils.equals( + value, CalendarCache.TIMEZONE_TYPE_AUTO) + if (useHomeTZ != mUseHomeTZ) { + writePrefs = true + mUseHomeTZ = useHomeTZ + } + } else if (TextUtils.equals( + key, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)) { + if (!TextUtils.isEmpty(value) && !TextUtils.equals(mHomeTZ, value)) { + writePrefs = true + mHomeTZ = value + } + } + } + cursor.close() + if (writePrefs) { + val prefs: SharedPreferences = + getSharedPreferences(cookie as Context, mPrefsName) + // Write the prefs + setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ) + setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ) + } + mTZQueryInProgress = false + for (callback in mTZCallbacks) { + if (callback != null) { + callback.run() + } + } + mTZCallbacks.clear() + } + } + } + + /** + * Formats a date or a time range according to the local conventions. + * + * This formats a date/time range using Calendar's time zone and the + * local conventions for the region of the device. + * + * If the [DateUtils.FORMAT_UTC] flag is used it will pass in + * the UTC time zone instead. + * + * @param context the context is required only if the time is shown + * @param startMillis the start time in UTC milliseconds + * @param endMillis the end time in UTC milliseconds + * @param flags a bit mask of options See + * [formatDateRange][DateUtils.formatDateRange] + * @return a string containing the formatted date/time range. + */ + fun formatDateRange(context: Context, startMillis: Long, + endMillis: Long, flags: Int): String { + var date: String + val tz: String + tz = if (flags and DateUtils.FORMAT_UTC !== 0) { + Time.TIMEZONE_UTC + } else { + getTimeZone(context, null) + } + synchronized(mSB) { + mSB.setLength(0) + date = DateUtils.formatDateRange(context, mF, startMillis, endMillis, flags, + tz).toString() + } + return date + } + + /** + * Writes a new home time zone to the db. + * + * Updates the home time zone in the db asynchronously and updates + * the local cache. Sending a time zone of + * [CalendarCache.TIMEZONE_TYPE_AUTO] will cause it to be set + * to the device's time zone. null or empty tz will be ignored. + * + * @param context The calling activity + * @param timeZone The time zone to set Calendar to, or + * [CalendarCache.TIMEZONE_TYPE_AUTO] + */ + fun setTimeZone(context: Context, timeZone: String) { + if (TextUtils.isEmpty(timeZone)) { + if (DEBUG) { + Log.d(TAG, "Empty time zone, nothing to be done.") + } + return + } + var updatePrefs = false + synchronized(mTZCallbacks) { + if (CalendarCache.TIMEZONE_TYPE_AUTO.equals(timeZone)) { + if (mUseHomeTZ) { + updatePrefs = true + } + mUseHomeTZ = false + } else { + if (!mUseHomeTZ || !TextUtils.equals(mHomeTZ, timeZone)) { + updatePrefs = true + } + mUseHomeTZ = true + mHomeTZ = timeZone + } + } + if (updatePrefs) { + // Write the prefs + val prefs: SharedPreferences = getSharedPreferences(context, mPrefsName) + setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ) + setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ) + + // Update the db + val values = ContentValues() + if (mHandler != null) { + mHandler?.cancelOperation(mToken) + } + mHandler = AsyncTZHandler(context.getContentResolver()) + + // skip 0 so query can use it + if (++mToken == 0) { + mToken = 1 + } + + // Write the use home tz setting + values.put(CalendarCache.VALUE, if (mUseHomeTZ) CalendarCache.TIMEZONE_TYPE_HOME + else CalendarCache.TIMEZONE_TYPE_AUTO) + mHandler?.startUpdate(mToken, null, CalendarCache.URI, values, "key=?", + TIMEZONE_TYPE_ARGS) + + // If using a home tz write it to the db + if (mUseHomeTZ) { + val values2 = ContentValues() + values2.put(CalendarCache.VALUE, mHomeTZ) + mHandler?.startUpdate(mToken, null, CalendarCache.URI, values2, + "key=?", TIMEZONE_INSTANCES_ARGS) + } + } + } + + /** + * Gets the time zone that Calendar should be displayed in + * + * This is a helper method to get the appropriate time zone for Calendar. If this + * is the first time this method has been called it will initiate an asynchronous + * query to verify that the data in preferences is correct. The callback supplied + * will only be called if this query returns a value other than what is stored in + * preferences and should cause the calling activity to refresh anything that + * depends on calling this method. + * + * @param context The calling activity + * @param callback The runnable that should execute if a query returns new values + * @return The string value representing the time zone Calendar should display + */ + fun getTimeZone(context: Context, callback: Runnable?): String { + synchronized(mTZCallbacks) { + if (mFirstTZRequest) { + val prefs: SharedPreferences = getSharedPreferences(context, mPrefsName) + mUseHomeTZ = prefs.getBoolean(KEY_HOME_TZ_ENABLED, false) + mHomeTZ = prefs.getString(KEY_HOME_TZ, Time.getCurrentTimezone()) ?: String() + + // Only check content resolver if we have a looper to attach to use + if (Looper.myLooper() != null) { + mTZQueryInProgress = true + mFirstTZRequest = false + + // When the async query returns it should synchronize on + // mTZCallbacks, update mUseHomeTZ, mHomeTZ, and the + // preferences, set mTZQueryInProgress to false, and call all + // the runnables in mTZCallbacks. + if (mHandler == null) { + mHandler = AsyncTZHandler(context.getContentResolver()) + } + mHandler?.startQuery(0, context, CalendarCache.URI, + CALENDAR_CACHE_POJECTION, null, null, null) + } + } + if (mTZQueryInProgress && callback != null) { + mTZCallbacks.add(callback) + } + } + return if (mUseHomeTZ) mHomeTZ else Time.getCurrentTimezone() + } + + /** + * Forces a query of the database to check for changes to the time zone. + * This should be called if another app may have modified the db. If a + * query is already in progress the callback will be added to the list + * of callbacks to be called when it returns. + * + * @param context The calling activity + * @param callback The runnable that should execute if a query returns + * new values + */ + fun forceDBRequery(context: Context, callback: Runnable) { + synchronized(mTZCallbacks) { + if (mTZQueryInProgress) { + mTZCallbacks.add(callback) + return + } + mFirstTZRequest = true + getTimeZone(context, callback) + } + } + + companion object { + private val TIMEZONE_TYPE_ARGS = arrayOf<String>(CalendarCache.KEY_TIMEZONE_TYPE) + private val TIMEZONE_INSTANCES_ARGS = + arrayOf<String>(CalendarCache.KEY_TIMEZONE_INSTANCES) + val CALENDAR_CACHE_POJECTION = arrayOf<String>( + CalendarCache.KEY, CalendarCache.VALUE + ) + private val mSB: StringBuilder = StringBuilder(50) + private val mF: Formatter = Formatter(mSB, Locale.getDefault()) + + @Volatile + private var mFirstTZRequest = true + + @Volatile + private var mTZQueryInProgress = false + + @Volatile + private var mUseHomeTZ = false + + @Volatile + private var mHomeTZ: String = Time.getCurrentTimezone() + private val mTZCallbacks: HashSet<Runnable> = HashSet<Runnable>() + private var mToken = 1 + private var mHandler: AsyncTZHandler? = null + + /** + * This is the key used for writing whether or not a home time zone should + * be used in the Calendar app to the Calendar Preferences. + */ + const val KEY_HOME_TZ_ENABLED = "preferences_home_tz_enabled" + + /** + * This is the key used for writing the time zone that should be used if + * home time zones are enabled for the Calendar app. + */ + const val KEY_HOME_TZ = "preferences_home_tz" + } + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/CalendarViewAdapter.java b/src/com/android/calendar/CalendarViewAdapter.java deleted file mode 100644 index 524268fc..00000000 --- a/src/com/android/calendar/CalendarViewAdapter.java +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright (C) 2011 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.calendar; - -import com.android.calendar.CalendarController.ViewType; - -import android.content.Context; -import android.os.Handler; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.TextView; - -import java.util.Formatter; -import java.util.Locale; - - -/* - * The MenuSpinnerAdapter defines the look of the ActionBar's pull down menu - * for small screen layouts. The pull down menu replaces the tabs uses for big screen layouts - * - * The MenuSpinnerAdapter responsible for creating the views used for in the pull down menu. - */ - -public class CalendarViewAdapter extends BaseAdapter { - - private static final String TAG = "MenuSpinnerAdapter"; - - private final String mButtonNames []; // Text on buttons - - // Used to define the look of the menu button according to the current view: - // Day view: show day of the week + full date underneath - // Week view: show the month + year - // Month view: show the month + year - // Agenda view: show day of the week + full date underneath - private int mCurrentMainView; - - private final LayoutInflater mInflater; - - // Defines the types of view returned by this spinner - private static final int BUTTON_VIEW_TYPE = 0; - static final int VIEW_TYPE_NUM = 1; // Increase this if you add more view types - - public static final int DAY_BUTTON_INDEX = 0; - public static final int WEEK_BUTTON_INDEX = 1; - public static final int MONTH_BUTTON_INDEX = 2; - public static final int AGENDA_BUTTON_INDEX = 3; - - // The current selected event's time, used to calculate the date and day of the week - // for the buttons. - private long mMilliTime; - private String mTimeZone; - private long mTodayJulianDay; - - private final Context mContext; - private final Formatter mFormatter; - private final StringBuilder mStringBuilder; - private Handler mMidnightHandler = null; // Used to run a time update every midnight - private final boolean mShowDate; // Spinner mode indicator (view name or view name with date) - - // Updates time specific variables (time-zone, today's Julian day). - private final Runnable mTimeUpdater = new Runnable() { - @Override - public void run() { - refresh(mContext); - } - }; - - public CalendarViewAdapter(Context context, int viewType, boolean showDate) { - super(); - - mMidnightHandler = new Handler(); - mCurrentMainView = viewType; - mContext = context; - mShowDate = showDate; - - // Initialize - mButtonNames = context.getResources().getStringArray(R.array.buttons_list); - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mStringBuilder = new StringBuilder(50); - mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); - - // Sets time specific variables and starts a thread for midnight updates - if (showDate) { - refresh(context); - } - } - - - // Sets the time zone and today's Julian day to be used by the adapter. - // Also, notify listener on the change and resets the midnight update thread. - public void refresh(Context context) { - mTimeZone = Utils.getTimeZone(context, mTimeUpdater); - Time time = new Time(mTimeZone); - long now = System.currentTimeMillis(); - time.set(now); - mTodayJulianDay = Time.getJulianDay(now, time.gmtoff); - notifyDataSetChanged(); - setMidnightHandler(); - } - - // Sets a thread to run 1 second after midnight and update the current date - // This is used to display correctly the date of yesterday/today/tomorrow - private void setMidnightHandler() { - mMidnightHandler.removeCallbacks(mTimeUpdater); - // Set the time updater to run at 1 second after midnight - long now = System.currentTimeMillis(); - Time time = new Time(mTimeZone); - time.set(now); - long runInMillis = (24 * 3600 - time.hour * 3600 - time.minute * 60 - - time.second + 1) * 1000; - mMidnightHandler.postDelayed(mTimeUpdater, runInMillis); - } - - // Stops the midnight update thread, called by the activity when it is paused. - public void onPause() { - mMidnightHandler.removeCallbacks(mTimeUpdater); - } - - // Returns the amount of buttons in the menu - @Override - public int getCount() { - return mButtonNames.length; - } - - - @Override - public Object getItem(int position) { - if (position < mButtonNames.length) { - return mButtonNames[position]; - } - return null; - } - - @Override - public long getItemId(int position) { - // Item ID is its location in the list - return position; - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - - View v; - - if (mShowDate) { - // Check if can recycle the view - if (convertView == null || ((Integer) convertView.getTag()).intValue() - != R.layout.actionbar_pulldown_menu_top_button) { - v = mInflater.inflate(R.layout.actionbar_pulldown_menu_top_button, parent, false); - // Set the tag to make sure you can recycle it when you get it - // as a convert view - v.setTag(new Integer(R.layout.actionbar_pulldown_menu_top_button)); - } else { - v = convertView; - } - TextView weekDay = (TextView) v.findViewById(R.id.top_button_weekday); - TextView date = (TextView) v.findViewById(R.id.top_button_date); - - switch (mCurrentMainView) { - case ViewType.DAY: - weekDay.setVisibility(View.VISIBLE); - weekDay.setText(buildDayOfWeek()); - date.setText(buildFullDate()); - break; - case ViewType.WEEK: - if (Utils.getShowWeekNumber(mContext)) { - weekDay.setVisibility(View.VISIBLE); - weekDay.setText(buildWeekNum()); - } else { - weekDay.setVisibility(View.GONE); - } - date.setText(buildMonthYearDate()); - break; - case ViewType.MONTH: - weekDay.setVisibility(View.GONE); - date.setText(buildMonthYearDate()); - break; - default: - v = null; - break; - } - } else { - if (convertView == null || ((Integer) convertView.getTag()).intValue() - != R.layout.actionbar_pulldown_menu_top_button_no_date) { - v = mInflater.inflate( - R.layout.actionbar_pulldown_menu_top_button_no_date, parent, false); - // Set the tag to make sure you can recycle it when you get it - // as a convert view - v.setTag(new Integer(R.layout.actionbar_pulldown_menu_top_button_no_date)); - } else { - v = convertView; - } - TextView title = (TextView) v; - switch (mCurrentMainView) { - case ViewType.DAY: - title.setText(mButtonNames [DAY_BUTTON_INDEX]); - break; - case ViewType.WEEK: - title.setText(mButtonNames [WEEK_BUTTON_INDEX]); - break; - case ViewType.MONTH: - title.setText(mButtonNames [MONTH_BUTTON_INDEX]); - break; - default: - v = null; - break; - } - } - return v; - } - - @Override - public int getItemViewType(int position) { - // Only one kind of view is used - return BUTTON_VIEW_TYPE; - } - - @Override - public int getViewTypeCount() { - return VIEW_TYPE_NUM; - } - - @Override - public boolean isEmpty() { - return (mButtonNames.length == 0); - } - - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - View v = mInflater.inflate(R.layout.actionbar_pulldown_menu_button, parent, false); - TextView viewType = (TextView)v.findViewById(R.id.button_view); - TextView date = (TextView)v.findViewById(R.id.button_date); - switch (position) { - case DAY_BUTTON_INDEX: - viewType.setText(mButtonNames [DAY_BUTTON_INDEX]); - if (mShowDate) { - date.setText(buildMonthDayDate()); - } - break; - case WEEK_BUTTON_INDEX: - viewType.setText(mButtonNames [WEEK_BUTTON_INDEX]); - if (mShowDate) { - date.setText(buildWeekDate()); - } - break; - case MONTH_BUTTON_INDEX: - viewType.setText(mButtonNames [MONTH_BUTTON_INDEX]); - if (mShowDate) { - date.setText(buildMonthDate()); - } - break; - default: - v = convertView; - break; - } - return v; - } - - // Updates the current viewType - // Used to match the label on the menu button with the calendar view - public void setMainView(int viewType) { - mCurrentMainView = viewType; - notifyDataSetChanged(); - } - - // Update the date that is displayed on buttons - // Used when the user selects a new day/week/month to watch - public void setTime(long time) { - mMilliTime = time; - notifyDataSetChanged(); - } - - // Builds a string with the day of the week and the word yesterday/today/tomorrow - // before it if applicable. - private String buildDayOfWeek() { - - Time t = new Time(mTimeZone); - t.set(mMilliTime); - long julianDay = Time.getJulianDay(mMilliTime,t.gmtoff); - String dayOfWeek = null; - mStringBuilder.setLength(0); - - if (julianDay == mTodayJulianDay) { - dayOfWeek = mContext.getString(R.string.agenda_today, - DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, - DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString()); - } else if (julianDay == mTodayJulianDay - 1) { - dayOfWeek = mContext.getString(R.string.agenda_yesterday, - DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, - DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString()); - } else if (julianDay == mTodayJulianDay + 1) { - dayOfWeek = mContext.getString(R.string.agenda_tomorrow, - DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, - DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString()); - } else { - dayOfWeek = DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, - DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString(); - } - return dayOfWeek.toUpperCase(); - } - - // Builds strings with different formats: - // Full date: Month,day Year - // Month year - // Month day - // Month - // Week: month day-day or month day - month day - private String buildFullDate() { - mStringBuilder.setLength(0); - String date = DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR, mTimeZone).toString(); - return date; - } - - private String buildMonthYearDate() { - mStringBuilder.setLength(0); - String date = DateUtils.formatDateRange( - mContext, - mFormatter, - mMilliTime, - mMilliTime, - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY - | DateUtils.FORMAT_SHOW_YEAR, mTimeZone).toString(); - return date; - } - - private String buildMonthDayDate() { - mStringBuilder.setLength(0); - String date = DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR, mTimeZone).toString(); - return date; - } - - private String buildMonthDate() { - mStringBuilder.setLength(0); - String date = DateUtils.formatDateRange( - mContext, - mFormatter, - mMilliTime, - mMilliTime, - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR - | DateUtils.FORMAT_NO_MONTH_DAY, mTimeZone).toString(); - return date; - } - - private String buildWeekDate() { - // Calculate the start of the week, taking into account the "first day of the week" - // setting. - - Time t = new Time(mTimeZone); - t.set(mMilliTime); - int firstDayOfWeek = Utils.getFirstDayOfWeek(mContext); - int dayOfWeek = t.weekDay; - int diff = dayOfWeek - firstDayOfWeek; - if (diff != 0) { - if (diff < 0) { - diff += 7; - } - t.monthDay -= diff; - t.normalize(true /* ignore isDst */); - } - - long weekStartTime = t.toMillis(true); - // The end of the week is 6 days after the start of the week - long weekEndTime = weekStartTime + DateUtils.WEEK_IN_MILLIS - DateUtils.DAY_IN_MILLIS; - - // If week start and end is in 2 different months, use short months names - Time t1 = new Time(mTimeZone); - t.set(weekEndTime); - int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR; - if (t.month != t1.month) { - flags |= DateUtils.FORMAT_ABBREV_MONTH; - } - - mStringBuilder.setLength(0); - String date = DateUtils.formatDateRange(mContext, mFormatter, weekStartTime, - weekEndTime, flags, mTimeZone).toString(); - return date; - } - - private String buildWeekNum() { - int week = Utils.getWeekNumberFromTime(mMilliTime, mContext); - return mContext.getResources().getQuantityString(R.plurals.weekN, week, week); - } - -} diff --git a/src/com/android/calendar/CalendarViewAdapter.kt b/src/com/android/calendar/CalendarViewAdapter.kt new file mode 100644 index 00000000..2fe10272 --- /dev/null +++ b/src/com/android/calendar/CalendarViewAdapter.kt @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2021 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.calendar + +import com.android.calendar.CalendarController.ViewType +import android.content.Context +import android.os.Handler +import android.text.format.DateUtils +import android.text.format.Time +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.TextView +import java.util.Formatter +import java.util.Locale + +/* + * The MenuSpinnerAdapter defines the look of the ActionBar's pull down menu + * for small screen layouts. The pull down menu replaces the tabs uses for big screen layouts + * + * The MenuSpinnerAdapter responsible for creating the views used for in the pull down menu. + */ +class CalendarViewAdapter(context: Context, viewType: Int, showDate: Boolean) : BaseAdapter() { + private val mButtonNames: Array<String> // Text on buttons + + // Used to define the look of the menu button according to the current view: + // Day view: show day of the week + full date underneath + // Week view: show the month + year + // Month view: show the month + year + // Agenda view: show day of the week + full date underneath + private var mCurrentMainView: Int + private val mInflater: LayoutInflater + + // The current selected event's time, used to calculate the date and day of the week + // for the buttons. + private var mMilliTime: Long = 0 + private var mTimeZone: String? = null + private var mTodayJulianDay: Long = 0 + private val mContext: Context = context + private val mFormatter: Formatter + private val mStringBuilder: StringBuilder + private var mMidnightHandler: Handler? = null // Used to run a time update every midnight + private val mShowDate: Boolean // Spinner mode indicator (view name or view name with date) + + // Updates time specific variables (time-zone, today's Julian day). + private val mTimeUpdater: Runnable = object : Runnable { + @Override + override fun run() { + refresh(mContext) + } + } + + // Sets the time zone and today's Julian day to be used by the adapter. + // Also, notify listener on the change and resets the midnight update thread. + fun refresh(context: Context?) { + mTimeZone = Utils.getTimeZone(context, mTimeUpdater) + val time = Time(mTimeZone) + val now: Long = System.currentTimeMillis() + time.set(now) + mTodayJulianDay = Time.getJulianDay(now, time.gmtoff).toLong() + notifyDataSetChanged() + setMidnightHandler() + } + + // Sets a thread to run 1 second after midnight and update the current date + // This is used to display correctly the date of yesterday/today/tomorrow + private fun setMidnightHandler() { + mMidnightHandler?.removeCallbacks(mTimeUpdater) + // Set the time updater to run at 1 second after midnight + val now: Long = System.currentTimeMillis() + val time = Time(mTimeZone) + time.set(now) + val runInMillis: Long = ((24 * 3600 - time.hour * 3600 - time.minute * 60 - + time.second + 1) * 1000).toLong() + mMidnightHandler?.postDelayed(mTimeUpdater, runInMillis) + } + + // Stops the midnight update thread, called by the activity when it is paused. + fun onPause() { + mMidnightHandler?.removeCallbacks(mTimeUpdater) + } + + // Returns the amount of buttons in the menu + @Override + override fun getCount(): Int { + return mButtonNames.size + } + + @Override + override fun getItem(position: Int): Any? { + return if (position < mButtonNames.size) { + mButtonNames[position] + } else null + } + + @Override + override fun getItemId(position: Int): Long { + // Item ID is its location in the list + return position.toLong() + } + + @Override + override fun hasStableIds(): Boolean { + return false + } + + @Override + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { + var v: View? + if (mShowDate) { + // Check if can recycle the view + if (convertView == null || (convertView.getTag() as Int) + != R.layout.actionbar_pulldown_menu_top_button as Int) { + v = mInflater.inflate(R.layout.actionbar_pulldown_menu_top_button, parent, false) + // Set the tag to make sure you can recycle it when you get it + // as a convert view + v.setTag(Integer(R.layout.actionbar_pulldown_menu_top_button)) + } else { + v = convertView + } + val weekDay: TextView = v?.findViewById(R.id.top_button_weekday) as TextView + val date: TextView = v?.findViewById(R.id.top_button_date) as TextView + when (mCurrentMainView) { + ViewType.DAY -> { + weekDay.setVisibility(View.VISIBLE) + weekDay.setText(buildDayOfWeek()) + date.setText(buildFullDate()) + } + ViewType.WEEK -> { + if (Utils.getShowWeekNumber(mContext)) { + weekDay.setVisibility(View.VISIBLE) + weekDay.setText(buildWeekNum()) + } else { + weekDay.setVisibility(View.GONE) + } + date.setText(buildMonthYearDate()) + } + ViewType.MONTH -> { + weekDay.setVisibility(View.GONE) + date.setText(buildMonthYearDate()) + } + else -> v = null + } + } else { + if (convertView == null || (convertView.getTag() as Int) + != R.layout.actionbar_pulldown_menu_top_button_no_date as Int) { + v = mInflater.inflate( + R.layout.actionbar_pulldown_menu_top_button_no_date, parent, false) + // Set the tag to make sure you can recycle it when you get it + // as a convert view + v.setTag(Integer(R.layout.actionbar_pulldown_menu_top_button_no_date)) + } else { + v = convertView + } + val title: TextView? = v as TextView? + when (mCurrentMainView) { + ViewType.DAY -> title?.setText(mButtonNames[DAY_BUTTON_INDEX]) + ViewType.WEEK -> title?.setText(mButtonNames[WEEK_BUTTON_INDEX]) + ViewType.MONTH -> title?.setText(mButtonNames[MONTH_BUTTON_INDEX]) + else -> v = null + } + } + return v + } + + @Override + override fun getItemViewType(position: Int): Int { + // Only one kind of view is used + return BUTTON_VIEW_TYPE + } + + @Override + override fun getViewTypeCount(): Int { + return VIEW_TYPE_NUM + } + + @Override + override fun isEmpty(): Boolean { + return mButtonNames.size == 0 + } + + @Override + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View? { + var v: View? = mInflater.inflate(R.layout.actionbar_pulldown_menu_button, parent, false) + val viewType: TextView? = v?.findViewById(R.id.button_view) as? TextView + val date: TextView? = v?.findViewById(R.id.button_date) as? TextView + when (position) { + DAY_BUTTON_INDEX -> { + viewType?.setText(mButtonNames[DAY_BUTTON_INDEX]) + if (mShowDate) { + date?.setText(buildMonthDayDate()) + } + } + WEEK_BUTTON_INDEX -> { + viewType?.setText(mButtonNames[WEEK_BUTTON_INDEX]) + if (mShowDate) { + date?.setText(buildWeekDate()) + } + } + MONTH_BUTTON_INDEX -> { + viewType?.setText(mButtonNames[MONTH_BUTTON_INDEX]) + if (mShowDate) { + date?.setText(buildMonthDate()) + } + } + else -> v = convertView + } + return v + } + + // Updates the current viewType + // Used to match the label on the menu button with the calendar view + fun setMainView(viewType: Int) { + mCurrentMainView = viewType + notifyDataSetChanged() + } + + // Update the date that is displayed on buttons + // Used when the user selects a new day/week/month to watch + fun setTime(time: Long) { + mMilliTime = time + notifyDataSetChanged() + } + + // Builds a string with the day of the week and the word yesterday/today/tomorrow + // before it if applicable. + private fun buildDayOfWeek(): String { + val t = Time(mTimeZone) + t.set(mMilliTime) + val julianDay: Long = Time.getJulianDay(mMilliTime, t.gmtoff).toLong() + var dayOfWeek: String? = null + mStringBuilder.setLength(0) + dayOfWeek = if (julianDay == mTodayJulianDay) { + mContext.getString(R.string.agenda_today, + DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, + DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString()) + } else if (julianDay == mTodayJulianDay - 1) { + mContext.getString(R.string.agenda_yesterday, + DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, + DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString()) + } else if (julianDay == mTodayJulianDay + 1) { + mContext.getString(R.string.agenda_tomorrow, + DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, + DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString()) + } else { + DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, + DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString() + } + return dayOfWeek.toUpperCase() + } + + // Builds strings with different formats: + // Full date: Month,day Year + // Month year + // Month day + // Month + // Week: month day-day or month day - month day + private fun buildFullDate(): String { + mStringBuilder.setLength(0) + return DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR, mTimeZone).toString() + } + + private fun buildMonthYearDate(): String { + mStringBuilder.setLength(0) + return DateUtils.formatDateRange( + mContext, + mFormatter, + mMilliTime, + mMilliTime, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_MONTH_DAY + or DateUtils.FORMAT_SHOW_YEAR, mTimeZone).toString() + } + + private fun buildMonthDayDate(): String { + mStringBuilder.setLength(0) + return DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR, mTimeZone).toString() + } + + private fun buildMonthDate(): String { + mStringBuilder.setLength(0) + return DateUtils.formatDateRange( + mContext, + mFormatter, + mMilliTime, + mMilliTime, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR + or DateUtils.FORMAT_NO_MONTH_DAY, mTimeZone).toString() + } + + private fun buildWeekDate(): String { + // Calculate the start of the week, taking into account the "first day of the week" + // setting. + val t = Time(mTimeZone) + t.set(mMilliTime) + val firstDayOfWeek: Int = Utils.getFirstDayOfWeek(mContext) + val dayOfWeek: Int = t.weekDay + var diff = dayOfWeek - firstDayOfWeek + if (diff != 0) { + if (diff < 0) { + diff += 7 + } + t.monthDay -= diff + t.normalize(true /* ignore isDst */) + } + val weekStartTime: Long = t.toMillis(true) + // The end of the week is 6 days after the start of the week + val weekEndTime: Long = weekStartTime + DateUtils.WEEK_IN_MILLIS - DateUtils.DAY_IN_MILLIS + + // If week start and end is in 2 different months, use short months names + val t1 = Time(mTimeZone) + t.set(weekEndTime) + var flags: Int = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR + if (t.month !== t1.month) { + flags = flags or DateUtils.FORMAT_ABBREV_MONTH + } + mStringBuilder.setLength(0) + return DateUtils.formatDateRange(mContext, mFormatter, weekStartTime, + weekEndTime, flags, mTimeZone).toString() + } + + private fun buildWeekNum(): String { + val week: Int = Utils.getWeekNumberFromTime(mMilliTime, mContext) + return mContext.getResources().getQuantityString(R.plurals.weekN, week, week) + } + + companion object { + private const val TAG = "MenuSpinnerAdapter" + + // Defines the types of view returned by this spinner + private const val BUTTON_VIEW_TYPE = 0 + const val VIEW_TYPE_NUM = 1 // Increase this if you add more view types + const val DAY_BUTTON_INDEX = 0 + const val WEEK_BUTTON_INDEX = 1 + const val MONTH_BUTTON_INDEX = 2 + const val AGENDA_BUTTON_INDEX = 3 + } + + init { + mMidnightHandler = Handler() + mCurrentMainView = viewType + mShowDate = showDate + + // Initialize + mButtonNames = context.getResources().getStringArray(R.array.buttons_list) + mInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + mStringBuilder = StringBuilder(50) + mFormatter = Formatter(mStringBuilder, Locale.getDefault()) + + // Sets time specific variables and starts a thread for midnight updates + if (showDate) { + refresh(context) + } + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/DayFragment.java b/src/com/android/calendar/DayFragment.java deleted file mode 100644 index a9fb39ed..00000000 --- a/src/com/android/calendar/DayFragment.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar; - -import com.android.calendar.CalendarController.EventInfo; -import com.android.calendar.CalendarController.EventType; - -import android.app.Fragment; -import android.content.Context; -import android.os.Bundle; -import android.text.format.Time; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.ProgressBar; -import android.widget.ViewSwitcher; -import android.widget.ViewSwitcher.ViewFactory; - -/** - * This is the base class for Day and Week Activities. - */ -public class DayFragment extends Fragment implements CalendarController.EventHandler, ViewFactory { - /** - * The view id used for all the views we create. It's OK to have all child - * views have the same ID. This ID is used to pick which view receives - * focus when a view hierarchy is saved / restore - */ - private static final int VIEW_ID = 1; - - protected static final String BUNDLE_KEY_RESTORE_TIME = "key_restore_time"; - - protected ProgressBar mProgressBar; - protected ViewSwitcher mViewSwitcher; - protected Animation mInAnimationForward; - protected Animation mOutAnimationForward; - protected Animation mInAnimationBackward; - protected Animation mOutAnimationBackward; - EventLoader mEventLoader; - - Time mSelectedDay = new Time(); - - private final Runnable mTZUpdater = new Runnable() { - @Override - public void run() { - if (!DayFragment.this.isAdded()) { - return; - } - String tz = Utils.getTimeZone(getActivity(), mTZUpdater); - mSelectedDay.timezone = tz; - mSelectedDay.normalize(true); - } - }; - - private int mNumDays; - - public DayFragment() { - mSelectedDay.setToNow(); - } - - public DayFragment(long timeMillis, int numOfDays) { - mNumDays = numOfDays; - if (timeMillis == 0) { - mSelectedDay.setToNow(); - } else { - mSelectedDay.set(timeMillis); - } - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - Context context = getActivity(); - - mInAnimationForward = AnimationUtils.loadAnimation(context, R.anim.slide_left_in); - mOutAnimationForward = AnimationUtils.loadAnimation(context, R.anim.slide_left_out); - mInAnimationBackward = AnimationUtils.loadAnimation(context, R.anim.slide_right_in); - mOutAnimationBackward = AnimationUtils.loadAnimation(context, R.anim.slide_right_out); - - mEventLoader = new EventLoader(context); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.day_activity, null); - - mViewSwitcher = (ViewSwitcher) v.findViewById(R.id.switcher); - mViewSwitcher.setFactory(this); - mViewSwitcher.getCurrentView().requestFocus(); - ((DayView) mViewSwitcher.getCurrentView()).updateTitle(); - - return v; - } - - public View makeView() { - mTZUpdater.run(); - DayView view = new DayView(getActivity(), CalendarController - .getInstance(getActivity()), mViewSwitcher, mEventLoader, mNumDays); - view.setId(VIEW_ID); - view.setLayoutParams(new ViewSwitcher.LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - view.setSelected(mSelectedDay, false, false); - return view; - } - - @Override - public void onResume() { - super.onResume(); - mEventLoader.startBackgroundThread(); - mTZUpdater.run(); - eventsChanged(); - DayView view = (DayView) mViewSwitcher.getCurrentView(); - view.handleOnResume(); - view.restartCurrentTimeUpdates(); - - view = (DayView) mViewSwitcher.getNextView(); - view.handleOnResume(); - view.restartCurrentTimeUpdates(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - public void onPause() { - super.onPause(); - DayView view = (DayView) mViewSwitcher.getCurrentView(); - view.cleanup(); - view = (DayView) mViewSwitcher.getNextView(); - view.cleanup(); - mEventLoader.stopBackgroundThread(); - - // Stop events cross-fade animation - view.stopEventsAnimation(); - ((DayView) mViewSwitcher.getNextView()).stopEventsAnimation(); - } - - void startProgressSpinner() { - // start the progress spinner - mProgressBar.setVisibility(View.VISIBLE); - } - - void stopProgressSpinner() { - // stop the progress spinner - mProgressBar.setVisibility(View.GONE); - } - - private void goTo(Time goToTime, boolean ignoreTime, boolean animateToday) { - if (mViewSwitcher == null) { - // The view hasn't been set yet. Just save the time and use it later. - mSelectedDay.set(goToTime); - return; - } - - DayView currentView = (DayView) mViewSwitcher.getCurrentView(); - - // How does goTo time compared to what's already displaying? - int diff = currentView.compareToVisibleTimeRange(goToTime); - - if (diff == 0) { - // In visible range. No need to switch view - currentView.setSelected(goToTime, ignoreTime, animateToday); - } else { - // Figure out which way to animate - if (diff > 0) { - mViewSwitcher.setInAnimation(mInAnimationForward); - mViewSwitcher.setOutAnimation(mOutAnimationForward); - } else { - mViewSwitcher.setInAnimation(mInAnimationBackward); - mViewSwitcher.setOutAnimation(mOutAnimationBackward); - } - - DayView next = (DayView) mViewSwitcher.getNextView(); - if (ignoreTime) { - next.setFirstVisibleHour(currentView.getFirstVisibleHour()); - } - - next.setSelected(goToTime, ignoreTime, animateToday); - next.reloadEvents(); - mViewSwitcher.showNext(); - next.requestFocus(); - next.updateTitle(); - next.restartCurrentTimeUpdates(); - } - } - - /** - * Returns the selected time in milliseconds. The milliseconds are measured - * in UTC milliseconds from the epoch and uniquely specifies any selectable - * time. - * - * @return the selected time in milliseconds - */ - public long getSelectedTimeInMillis() { - if (mViewSwitcher == null) { - return -1; - } - DayView view = (DayView) mViewSwitcher.getCurrentView(); - if (view == null) { - return -1; - } - return view.getSelectedTimeInMillis(); - } - - public void eventsChanged() { - if (mViewSwitcher == null) { - return; - } - DayView view = (DayView) mViewSwitcher.getCurrentView(); - view.clearCachedEvents(); - view.reloadEvents(); - - view = (DayView) mViewSwitcher.getNextView(); - view.clearCachedEvents(); - } - - public DayView getNextView() { - return (DayView) mViewSwitcher.getNextView(); - } - - public long getSupportedEventTypes() { - return EventType.GO_TO | EventType.EVENTS_CHANGED; - } - - public void handleEvent(EventInfo msg) { - if (msg.eventType == EventType.GO_TO) { -// TODO support a range of time -// TODO support event_id -// TODO support select message - goTo(msg.selectedTime, (msg.extraLong & CalendarController.EXTRA_GOTO_DATE) != 0, - (msg.extraLong & CalendarController.EXTRA_GOTO_TODAY) != 0); - } else if (msg.eventType == EventType.EVENTS_CHANGED) { - eventsChanged(); - } - } -} diff --git a/src/com/android/calendar/DayFragment.kt b/src/com/android/calendar/DayFragment.kt new file mode 100644 index 00000000..39e92f5b --- /dev/null +++ b/src/com/android/calendar/DayFragment.kt @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2021 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.calendar + +import com.android.calendar.CalendarController.EventInfo +import com.android.calendar.CalendarController.EventType +import android.app.Fragment +import android.content.Context +import android.os.Bundle +import android.text.format.Time +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout.LayoutParams +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import android.widget.ProgressBar +import android.widget.ViewSwitcher +import android.widget.ViewSwitcher.ViewFactory + +/** + * This is the base class for Day and Week Activities. + */ +class DayFragment : Fragment, CalendarController.EventHandler, ViewFactory { + protected var mProgressBar: ProgressBar? = null + protected var mViewSwitcher: ViewSwitcher? = null + protected var mInAnimationForward: Animation? = null + protected var mOutAnimationForward: Animation? = null + protected var mInAnimationBackward: Animation? = null + protected var mOutAnimationBackward: Animation? = null + var mEventLoader: EventLoader? = null + var mSelectedDay: Time = Time() + private val mTZUpdater: Runnable = object : Runnable { + override fun run() { + if (!this@DayFragment.isAdded()) { + return + } + val tz: String? = Utils.getTimeZone(getActivity(), this) + mSelectedDay.timezone = tz + mSelectedDay.normalize(true) + } + } + private var mNumDays = 0 + + constructor() { + mSelectedDay.setToNow() + } + + constructor(timeMillis: Long, numOfDays: Int) { + mNumDays = numOfDays + if (timeMillis == 0L) { + mSelectedDay.setToNow() + } else { + mSelectedDay.set(timeMillis) + } + } + + override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + val context: Context = getActivity() + mInAnimationForward = AnimationUtils.loadAnimation(context, R.anim.slide_left_in) + mOutAnimationForward = AnimationUtils.loadAnimation(context, R.anim.slide_left_out) + mInAnimationBackward = AnimationUtils.loadAnimation(context, R.anim.slide_right_in) + mOutAnimationBackward = AnimationUtils.loadAnimation(context, R.anim.slide_right_out) + mEventLoader = EventLoader(context) + } + + override fun onCreateView( + inflater: LayoutInflater?, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val v: View? = inflater?.inflate(R.layout.day_activity, null) + mViewSwitcher = v?.findViewById(R.id.switcher) as? ViewSwitcher + mViewSwitcher?.setFactory(this) + mViewSwitcher?.getCurrentView()?.requestFocus() + (mViewSwitcher?.getCurrentView() as? DayView)?.updateTitle() + return v + } + + override fun makeView(): View { + mTZUpdater.run() + val view = DayView(getActivity(), CalendarController + .getInstance(getActivity()), mViewSwitcher, mEventLoader, mNumDays) + view.setId(DayFragment.Companion.VIEW_ID) + view.setLayoutParams(LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) + view.setSelected(mSelectedDay, false, false) + return view + } + + override fun onResume() { + super.onResume() + mEventLoader!!.startBackgroundThread() + mTZUpdater.run() + eventsChanged() + var view: DayView? = mViewSwitcher?.getCurrentView() as? DayView + view?.handleOnResume() + view?.restartCurrentTimeUpdates() + view = mViewSwitcher?.getNextView() as? DayView + view?.handleOnResume() + view?.restartCurrentTimeUpdates() + } + + override fun onSaveInstanceState(outState: Bundle?) { + super.onSaveInstanceState(outState) + } + + override fun onPause() { + super.onPause() + var view: DayView? = mViewSwitcher?.getCurrentView() as? DayView + view?.cleanup() + view = mViewSwitcher?.getNextView() as? DayView + view?.cleanup() + mEventLoader!!.stopBackgroundThread() + + // Stop events cross-fade animation + view?.stopEventsAnimation() + (mViewSwitcher?.getNextView() as? DayView)?.stopEventsAnimation() + } + + fun startProgressSpinner() { + // start the progress spinner + mProgressBar?.setVisibility(View.VISIBLE) + } + + fun stopProgressSpinner() { + // stop the progress spinner + mProgressBar?.setVisibility(View.GONE) + } + + private fun goTo(goToTime: Time?, ignoreTime: Boolean, animateToday: Boolean) { + if (mViewSwitcher == null) { + // The view hasn't been set yet. Just save the time and use it later. + mSelectedDay.set(goToTime) + return + } + val currentView: DayView? = mViewSwitcher?.getCurrentView() as? DayView + + // How does goTo time compared to what's already displaying? + val diff: Int = currentView?.compareToVisibleTimeRange(goToTime as Time) as Int + if (diff == 0) { + // In visible range. No need to switch view + currentView?.setSelected(goToTime, ignoreTime, animateToday) + } else { + // Figure out which way to animate + if (diff > 0) { + mViewSwitcher?.setInAnimation(mInAnimationForward) + mViewSwitcher?.setOutAnimation(mOutAnimationForward) + } else { + mViewSwitcher?.setInAnimation(mInAnimationBackward) + mViewSwitcher?.setOutAnimation(mOutAnimationBackward) + } + val next: DayView? = mViewSwitcher?.getNextView() as? DayView + if (ignoreTime) { + next!!.firstVisibleHour = currentView.firstVisibleHour + } + next?.setSelected(goToTime, ignoreTime, animateToday) + next?.reloadEvents() + mViewSwitcher?.showNext() + next?.requestFocus() + next?.updateTitle() + next?.restartCurrentTimeUpdates() + } + } + + /** + * Returns the selected time in milliseconds. The milliseconds are measured + * in UTC milliseconds from the epoch and uniquely specifies any selectable + * time. + * + * @return the selected time in milliseconds + */ + val selectedTimeInMillis: Long + get() { + if (mViewSwitcher == null) { + return -1 + } + val view: DayView = mViewSwitcher?.getCurrentView() as DayView ?: return -1 + return view.selectedTimeInMillis + } + + override fun eventsChanged() { + if (mViewSwitcher == null) { + return + } + var view: DayView? = mViewSwitcher?.getCurrentView() as? DayView + view?.clearCachedEvents() + view?.reloadEvents() + view = mViewSwitcher?.getNextView() as? DayView + view?.clearCachedEvents() + } + + val nextView: DayView? + get() = mViewSwitcher?.getNextView() as? DayView + override val supportedEventTypes: Long + get() = CalendarController.EventType.GO_TO or CalendarController.EventType.EVENTS_CHANGED + + override fun handleEvent(msg: CalendarController.EventInfo?) { + if (msg?.eventType == CalendarController.EventType.GO_TO) { +// TODO support a range of time +// TODO support event_id +// TODO support select message + goTo(msg?.selectedTime, msg?.extraLong and CalendarController.EXTRA_GOTO_DATE != 0L, + msg?.extraLong and CalendarController.EXTRA_GOTO_TODAY != 0L) + } else if (msg?.eventType == CalendarController.EventType.EVENTS_CHANGED) { + eventsChanged() + } + } + + companion object { + /** + * The view id used for all the views we create. It's OK to have all child + * views have the same ID. This ID is used to pick which view receives + * focus when a view hierarchy is saved / restore + */ + private const val VIEW_ID = 1 + protected const val BUNDLE_KEY_RESTORE_TIME = "key_restore_time" + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/DayOfMonthDrawable.java b/src/com/android/calendar/DayOfMonthDrawable.java deleted file mode 100644 index 461ab317..00000000 --- a/src/com/android/calendar/DayOfMonthDrawable.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.calendar; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; - -/** - * A custom view to draw the day of the month in the today button in the options menu - */ - -public class DayOfMonthDrawable extends Drawable { - - private String mDayOfMonth = "1"; - private final Paint mPaint; - private final Rect mTextBounds = new Rect(); - private static float mTextSize = 14; - - public DayOfMonthDrawable(Context c) { - mTextSize = c.getResources().getDimension(R.dimen.today_icon_text_size); - mPaint = new Paint(); - mPaint.setAlpha(255); - mPaint.setColor(0xFF777777); - mPaint.setTypeface(Typeface.DEFAULT_BOLD); - mPaint.setTextSize(mTextSize); - mPaint.setTextAlign(Paint.Align.CENTER); - } - - @Override - public void draw(Canvas canvas) { - mPaint.getTextBounds(mDayOfMonth, 0, mDayOfMonth.length(), mTextBounds); - int textHeight = mTextBounds.bottom - mTextBounds.top; - Rect bounds = getBounds(); - canvas.drawText(mDayOfMonth, bounds.right / 2, ((float) bounds.bottom + textHeight + 1) / 2, - mPaint); - } - - @Override - public void setAlpha(int alpha) { - mPaint.setAlpha(alpha); - } - - @Override - public void setColorFilter(ColorFilter cf) { - // Ignore - } - - @Override - public int getOpacity() { - return PixelFormat.UNKNOWN; - } - - public void setDayOfMonth(int day) { - mDayOfMonth = Integer.toString(day); - invalidateSelf(); - } -} diff --git a/src/com/android/calendar/DayOfMonthDrawable.kt b/src/com/android/calendar/DayOfMonthDrawable.kt new file mode 100644 index 00000000..e348b5a2 --- /dev/null +++ b/src/com/android/calendar/DayOfMonthDrawable.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.content.Context +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.Typeface +import android.graphics.drawable.Drawable + +/** + * A custom view to draw the day of the month in the today button in the options menu + */ +class DayOfMonthDrawable(c: Context) : Drawable() { + private var mDayOfMonth = "1" + private val mPaint: Paint + private val mTextBounds: Rect = Rect() + override fun draw(canvas: Canvas) { + mPaint.getTextBounds(mDayOfMonth, 0, mDayOfMonth.length, mTextBounds) + val textHeight: Int = mTextBounds.bottom - mTextBounds.top + val bounds: Rect = getBounds() + canvas.drawText( + mDayOfMonth, (bounds.right).toFloat() / 2f, ((bounds.bottom).toFloat() + + textHeight + 1) / 2f, mPaint + ) + } + + override fun setAlpha(alpha: Int) { + mPaint.setAlpha(alpha) + } + + override fun setColorFilter(cf: ColorFilter?) { + // Ignore + } + + override fun getOpacity(): Int { + return PixelFormat.UNKNOWN + } + + fun setDayOfMonth(day: Int) { + mDayOfMonth = Integer.toString(day) + invalidateSelf() + } + + companion object { + private var mTextSize = 14f + } + + init { + mTextSize = c.getResources().getDimension(R.dimen.today_icon_text_size) + mPaint = Paint() + mPaint.setAlpha(255) + mPaint.setColor(-0x888889) + mPaint.setTypeface(Typeface.DEFAULT_BOLD) + mPaint.setTextSize(mTextSize) + mPaint.setTextAlign(Paint.Align.CENTER) + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/DayView.java b/src/com/android/calendar/DayView.java deleted file mode 100644 index 2fc00b3c..00000000 --- a/src/com/android/calendar/DayView.java +++ /dev/null @@ -1,4008 +0,0 @@ -/* - * Copyright (C) 2007 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.calendar; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.app.AlertDialog; -import android.app.Service; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.database.Cursor; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Paint.Align; -import android.graphics.Paint.Style; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; -import android.provider.CalendarContract.Attendees; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Events; -import android.text.Layout.Alignment; -import android.text.SpannableStringBuilder; -import android.text.StaticLayout; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.text.style.StyleSpan; -import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.GestureDetector; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.ScaleGestureDetector; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.Animation; -import android.view.animation.Interpolator; -import android.view.animation.TranslateAnimation; -import android.widget.EdgeEffect; -import android.widget.ImageView; -import android.widget.OverScroller; -import android.widget.PopupWindow; -import android.widget.TextView; -import android.widget.ViewSwitcher; - -import com.android.calendar.CalendarController.EventType; -import com.android.calendar.CalendarController.ViewType; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Formatter; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * View for multi-day view. So far only 1 and 7 day have been tested. - */ -public class DayView extends View implements View.OnCreateContextMenuListener, - ScaleGestureDetector.OnScaleGestureListener, View.OnClickListener, View.OnLongClickListener - { - private static String TAG = "DayView"; - private static boolean DEBUG = false; - private static boolean DEBUG_SCALING = false; - private static final String PERIOD_SPACE = ". "; - - private static float mScale = 0; // Used for supporting different screen densities - private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event - // Duration of the allday expansion - private static final long ANIMATION_DURATION = 400; - // duration of the more allday event text fade - private static final long ANIMATION_SECONDARY_DURATION = 200; - // duration of the scroll to go to a specified time - private static final int GOTO_SCROLL_DURATION = 200; - // duration for events' cross-fade animation - private static final int EVENTS_CROSS_FADE_DURATION = 400; - // duration to show the event clicked - private static final int CLICK_DISPLAY_DURATION = 50; - - private static final int MENU_DAY = 3; - private static final int MENU_EVENT_VIEW = 5; - private static final int MENU_EVENT_CREATE = 6; - private static final int MENU_EVENT_EDIT = 7; - private static final int MENU_EVENT_DELETE = 8; - - private static int DEFAULT_CELL_HEIGHT = 64; - private static int MAX_CELL_HEIGHT = 150; - private static int MIN_Y_SPAN = 100; - - private boolean mOnFlingCalled; - private boolean mStartingScroll = false; - protected boolean mPaused = true; - private Handler mHandler; - /** - * ID of the last event which was displayed with the toast popup. - * - * This is used to prevent popping up multiple quick views for the same event, especially - * during calendar syncs. This becomes valid when an event is selected, either by default - * on starting calendar or by scrolling to an event. It becomes invalid when the user - * explicitly scrolls to an empty time slot, changes views, or deletes the event. - */ - private long mLastPopupEventID; - - protected Context mContext; - - private static final String[] CALENDARS_PROJECTION = new String[] { - Calendars._ID, // 0 - Calendars.CALENDAR_ACCESS_LEVEL, // 1 - Calendars.OWNER_ACCOUNT, // 2 - }; - private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1; - private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; - private static final String CALENDARS_WHERE = Calendars._ID + "=%d"; - - private static final int FROM_NONE = 0; - private static final int FROM_ABOVE = 1; - private static final int FROM_BELOW = 2; - private static final int FROM_LEFT = 4; - private static final int FROM_RIGHT = 8; - - private static final int ACCESS_LEVEL_NONE = 0; - private static final int ACCESS_LEVEL_DELETE = 1; - private static final int ACCESS_LEVEL_EDIT = 2; - - private static int mHorizontalSnapBackThreshold = 128; - - private final ContinueScroll mContinueScroll = new ContinueScroll(); - - // Make this visible within the package for more informative debugging - Time mBaseDate; - private Time mCurrentTime; - //Update the current time line every five minutes if the window is left open that long - private static final int UPDATE_CURRENT_TIME_DELAY = 300000; - private final UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime(); - private int mTodayJulianDay; - - private final Typeface mBold = Typeface.DEFAULT_BOLD; - private int mFirstJulianDay; - private int mLoadedFirstJulianDay = -1; - private int mLastJulianDay; - - private int mMonthLength; - private int mFirstVisibleDate; - private int mFirstVisibleDayOfWeek; - private int[] mEarliestStartHour; // indexed by the week day offset - private boolean[] mHasAllDayEvent; // indexed by the week day offset - private String mEventCountTemplate; - private Event mClickedEvent; // The event the user clicked on - private Event mSavedClickedEvent; - private static int mOnDownDelay; - private int mClickedYLocation; - private long mDownTouchTime; - - private int mEventsAlpha = 255; - private ObjectAnimator mEventsCrossFadeAnimation; - - protected static StringBuilder mStringBuilder = new StringBuilder(50); - // TODO recreate formatter when locale changes - protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); - - private final Runnable mTZUpdater = new Runnable() { - @Override - public void run() { - String tz = Utils.getTimeZone(mContext, this); - mBaseDate.timezone = tz; - mBaseDate.normalize(true); - mCurrentTime.switchTimezone(tz); - invalidate(); - } - }; - - // Sets the "clicked" color from the clicked event - private final Runnable mSetClick = new Runnable() { - @Override - public void run() { - mClickedEvent = mSavedClickedEvent; - mSavedClickedEvent = null; - DayView.this.invalidate(); - } - }; - - // Clears the "clicked" color from the clicked event and launch the event - private final Runnable mClearClick = new Runnable() { - @Override - public void run() { - if (mClickedEvent != null) { - mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, mClickedEvent.id, - mClickedEvent.startMillis, mClickedEvent.endMillis, - DayView.this.getWidth() / 2, mClickedYLocation, - getSelectedTimeInMillis()); - } - mClickedEvent = null; - DayView.this.invalidate(); - } - }; - - private final TodayAnimatorListener mTodayAnimatorListener = new TodayAnimatorListener(); - - class TodayAnimatorListener extends AnimatorListenerAdapter { - private volatile Animator mAnimator = null; - private volatile boolean mFadingIn = false; - - @Override - public void onAnimationEnd(Animator animation) { - synchronized (this) { - if (mAnimator != animation) { - animation.removeAllListeners(); - animation.cancel(); - return; - } - if (mFadingIn) { - if (mTodayAnimator != null) { - mTodayAnimator.removeAllListeners(); - mTodayAnimator.cancel(); - } - mTodayAnimator = ObjectAnimator - .ofInt(DayView.this, "animateTodayAlpha", 255, 0); - mAnimator = mTodayAnimator; - mFadingIn = false; - mTodayAnimator.addListener(this); - mTodayAnimator.setDuration(600); - mTodayAnimator.start(); - } else { - mAnimateToday = false; - mAnimateTodayAlpha = 0; - mAnimator.removeAllListeners(); - mAnimator = null; - mTodayAnimator = null; - invalidate(); - } - } - } - - public void setAnimator(Animator animation) { - mAnimator = animation; - } - - public void setFadingIn(boolean fadingIn) { - mFadingIn = fadingIn; - } - - } - - AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mScrolling = true; - } - - @Override - public void onAnimationCancel(Animator animation) { - mScrolling = false; - } - - @Override - public void onAnimationEnd(Animator animation) { - mScrolling = false; - resetSelectedHour(); - invalidate(); - } - }; - - /** - * This variable helps to avoid unnecessarily reloading events by keeping - * track of the start millis parameter used for the most recent loading - * of events. If the next reload matches this, then the events are not - * reloaded. To force a reload, set this to zero (this is set to zero - * in the method clearCachedEvents()). - */ - private long mLastReloadMillis; - - private ArrayList<Event> mEvents = new ArrayList<Event>(); - private ArrayList<Event> mAllDayEvents = new ArrayList<Event>(); - private StaticLayout[] mLayouts = null; - private StaticLayout[] mAllDayLayouts = null; - private int mSelectionDay; // Julian day - private int mSelectionHour; - - boolean mSelectionAllday; - - // Current selection info for accessibility - private int mSelectionDayForAccessibility; // Julian day - private int mSelectionHourForAccessibility; - private Event mSelectedEventForAccessibility; - // Last selection info for accessibility - private int mLastSelectionDayForAccessibility; - private int mLastSelectionHourForAccessibility; - private Event mLastSelectedEventForAccessibility; - - - /** Width of a day or non-conflicting event */ - private int mCellWidth; - - // Pre-allocate these objects and re-use them - private final Rect mRect = new Rect(); - private final Rect mDestRect = new Rect(); - private final Rect mSelectionRect = new Rect(); - // This encloses the more allDay events icon - private final Rect mExpandAllDayRect = new Rect(); - // TODO Clean up paint usage - private final Paint mPaint = new Paint(); - private final Paint mEventTextPaint = new Paint(); - private final Paint mSelectionPaint = new Paint(); - private float[] mLines; - - private int mFirstDayOfWeek; // First day of the week - - private PopupWindow mPopup; - private View mPopupView; - - // The number of milliseconds to show the popup window - private static final int POPUP_DISMISS_DELAY = 3000; - private final DismissPopup mDismissPopup = new DismissPopup(); - - private boolean mRemeasure = true; - - private final EventLoader mEventLoader; - protected final EventGeometry mEventGeometry; - - private static float GRID_LINE_LEFT_MARGIN = 0; - private static final float GRID_LINE_INNER_WIDTH = 1; - - private static final int DAY_GAP = 1; - private static final int HOUR_GAP = 1; - // This is the standard height of an allday event with no restrictions - private static int SINGLE_ALLDAY_HEIGHT = 34; - /** - * This is the minimum desired height of a allday event. - * When unexpanded, allday events will use this height. - * When expanded allDay events will attempt to grow to fit all - * events at this height. - */ - private static float MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = 28.0F; // in pixels - /** - * This is how big the unexpanded allday height is allowed to be. - * It will get adjusted based on screen size - */ - private static int MAX_UNEXPANDED_ALLDAY_HEIGHT = - (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); - /** - * This is the minimum size reserved for displaying regular events. - * The expanded allDay region can't expand into this. - */ - private static int MIN_HOURS_HEIGHT = 180; - private static int ALLDAY_TOP_MARGIN = 1; - // The largest a single allDay event will become. - private static int MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34; - - private static int HOURS_TOP_MARGIN = 2; - private static int HOURS_LEFT_MARGIN = 2; - private static int HOURS_RIGHT_MARGIN = 4; - private static int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; - private static int NEW_EVENT_MARGIN = 4; - private static int NEW_EVENT_WIDTH = 2; - private static int NEW_EVENT_MAX_LENGTH = 16; - - private static int CURRENT_TIME_LINE_SIDE_BUFFER = 4; - private static int CURRENT_TIME_LINE_TOP_OFFSET = 2; - - /* package */ static final int MINUTES_PER_HOUR = 60; - /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24; - /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000; - /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000); - /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; - - // More events text will transition between invisible and this alpha - private static final int MORE_EVENTS_MAX_ALPHA = 0x4C; - private static int DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0; - private static int DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5; - private static int DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6; - private static int DAY_HEADER_RIGHT_MARGIN = 4; - private static int DAY_HEADER_BOTTOM_MARGIN = 3; - private static float DAY_HEADER_FONT_SIZE = 14; - private static float DATE_HEADER_FONT_SIZE = 32; - private static float NORMAL_FONT_SIZE = 12; - private static float EVENT_TEXT_FONT_SIZE = 12; - private static float HOURS_TEXT_SIZE = 12; - private static float AMPM_TEXT_SIZE = 9; - private static int MIN_HOURS_WIDTH = 96; - private static int MIN_CELL_WIDTH_FOR_TEXT = 20; - private static final int MAX_EVENT_TEXT_LEN = 500; - // smallest height to draw an event with - private static float MIN_EVENT_HEIGHT = 24.0F; // in pixels - private static int CALENDAR_COLOR_SQUARE_SIZE = 10; - private static int EVENT_RECT_TOP_MARGIN = 1; - private static int EVENT_RECT_BOTTOM_MARGIN = 0; - private static int EVENT_RECT_LEFT_MARGIN = 1; - private static int EVENT_RECT_RIGHT_MARGIN = 0; - private static int EVENT_RECT_STROKE_WIDTH = 2; - private static int EVENT_TEXT_TOP_MARGIN = 2; - private static int EVENT_TEXT_BOTTOM_MARGIN = 2; - private static int EVENT_TEXT_LEFT_MARGIN = 6; - private static int EVENT_TEXT_RIGHT_MARGIN = 6; - private static int ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1; - private static int EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; - private static int EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN; - private static int EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; - private static int EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN; - // margins and sizing for the expand allday icon - private static int EXPAND_ALL_DAY_BOTTOM_MARGIN = 10; - // sizing for "box +n" in allDay events - private static int EVENT_SQUARE_WIDTH = 10; - private static int EVENT_LINE_PADDING = 4; - private static int NEW_EVENT_HINT_FONT_SIZE = 12; - - private static int mEventTextColor; - private static int mMoreEventsTextColor; - - private static int mWeek_saturdayColor; - private static int mWeek_sundayColor; - private static int mCalendarDateBannerTextColor; - private static int mCalendarAmPmLabel; - private static int mCalendarGridAreaSelected; - private static int mCalendarGridLineInnerHorizontalColor; - private static int mCalendarGridLineInnerVerticalColor; - private static int mFutureBgColor; - private static int mFutureBgColorRes; - private static int mBgColor; - private static int mNewEventHintColor; - private static int mCalendarHourLabelColor; - private static int mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA; - - private float mAnimationDistance = 0; - private int mViewStartX; - private int mViewStartY; - private int mMaxViewStartY; - private int mViewHeight; - private int mViewWidth; - private int mGridAreaHeight = -1; - private static int mCellHeight = 0; // shared among all DayViews - private static int mMinCellHeight = 32; - private int mScrollStartY; - private int mPreviousDirection; - private static int mScaledPagingTouchSlop = 0; - - /** - * Vertical distance or span between the two touch points at the start of a - * scaling gesture - */ - private float mStartingSpanY = 0; - /** Height of 1 hour in pixels at the start of a scaling gesture */ - private int mCellHeightBeforeScaleGesture; - /** The hour at the center two touch points */ - private float mGestureCenterHour = 0; - - private boolean mRecalCenterHour = false; - - /** - * Flag to decide whether to handle the up event. Cases where up events - * should be ignored are 1) right after a scale gesture and 2) finger was - * down before app launch - */ - private boolean mHandleActionUp = true; - - private int mHoursTextHeight; - /** - * The height of the area used for allday events - */ - private int mAlldayHeight; - /** - * The height of the allday event area used during animation - */ - private int mAnimateDayHeight = 0; - /** - * The height of an individual allday event during animation - */ - private int mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; - /** - * Whether to use the expand or collapse icon. - */ - private static boolean mUseExpandIcon = true; - /** - * The height of the day names/numbers - */ - private static int DAY_HEADER_HEIGHT = 45; - /** - * The height of the day names/numbers for multi-day views - */ - private static int MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; - /** - * The height of the day names/numbers when viewing a single day - */ - private static int ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; - /** - * Max of all day events in a given day in this view. - */ - private int mMaxAlldayEvents; - /** - * A count of the number of allday events that were not drawn for each day - */ - private int[] mSkippedAlldayEvents; - /** - * The number of allDay events at which point we start hiding allDay events. - */ - private int mMaxUnexpandedAlldayEventCount = 4; - /** - * Whether or not to expand the allDay area to fill the screen - */ - private static boolean mShowAllAllDayEvents = false; - - protected int mNumDays = 7; - private int mNumHours = 10; - - /** Width of the time line (list of hours) to the left. */ - private int mHoursWidth; - private int mDateStrWidth; - /** Top of the scrollable region i.e. below date labels and all day events */ - private int mFirstCell; - /** First fully visibile hour */ - private int mFirstHour = -1; - /** Distance between the mFirstCell and the top of first fully visible hour. */ - private int mFirstHourOffset; - private String[] mHourStrs; - private String[] mDayStrs; - private String[] mDayStrs2Letter; - private boolean mIs24HourFormat; - - private final ArrayList<Event> mSelectedEvents = new ArrayList<Event>(); - private boolean mComputeSelectedEvents; - private boolean mUpdateToast; - private Event mSelectedEvent; - private Event mPrevSelectedEvent; - private final Rect mPrevBox = new Rect(); - protected final Resources mResources; - protected final Drawable mCurrentTimeLine; - protected final Drawable mCurrentTimeAnimateLine; - protected final Drawable mTodayHeaderDrawable; - protected final Drawable mExpandAlldayDrawable; - protected final Drawable mCollapseAlldayDrawable; - protected Drawable mAcceptedOrTentativeEventBoxDrawable; - private String mAmString; - private String mPmString; - private static int sCounter = 0; - - ScaleGestureDetector mScaleGestureDetector; - - /** - * The initial state of the touch mode when we enter this view. - */ - private static final int TOUCH_MODE_INITIAL_STATE = 0; - - /** - * Indicates we just received the touch event and we are waiting to see if - * it is a tap or a scroll gesture. - */ - private static final int TOUCH_MODE_DOWN = 1; - - /** - * Indicates the touch gesture is a vertical scroll - */ - private static final int TOUCH_MODE_VSCROLL = 0x20; - - /** - * Indicates the touch gesture is a horizontal scroll - */ - private static final int TOUCH_MODE_HSCROLL = 0x40; - - private int mTouchMode = TOUCH_MODE_INITIAL_STATE; - - /** - * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS. - */ - private static final int SELECTION_HIDDEN = 0; - private static final int SELECTION_PRESSED = 1; // D-pad down but not up yet - private static final int SELECTION_SELECTED = 2; - private static final int SELECTION_LONGPRESS = 3; - - private int mSelectionMode = SELECTION_HIDDEN; - - private boolean mScrolling = false; - - // Pixels scrolled - private float mInitialScrollX; - private float mInitialScrollY; - - private boolean mAnimateToday = false; - private int mAnimateTodayAlpha = 0; - - // Animates the height of the allday region - ObjectAnimator mAlldayAnimator; - // Animates the height of events in the allday region - ObjectAnimator mAlldayEventAnimator; - // Animates the transparency of the more events text - ObjectAnimator mMoreAlldayEventsAnimator; - // Animates the current time marker when Today is pressed - ObjectAnimator mTodayAnimator; - // whether or not an event is stopping because it was cancelled - private boolean mCancellingAnimations = false; - // tracks whether a touch originated in the allday area - private boolean mTouchStartedInAlldayArea = false; - - private final CalendarController mController; - private final ViewSwitcher mViewSwitcher; - private final GestureDetector mGestureDetector; - private final OverScroller mScroller; - private final EdgeEffect mEdgeEffectTop; - private final EdgeEffect mEdgeEffectBottom; - private boolean mCallEdgeEffectOnAbsorb; - private final int OVERFLING_DISTANCE; - private float mLastVelocity; - - private final ScrollInterpolator mHScrollInterpolator; - private AccessibilityManager mAccessibilityMgr = null; - private boolean mIsAccessibilityEnabled = false; - private boolean mTouchExplorationEnabled = false; - private final String mNewEventHintString; - - public DayView(Context context, CalendarController controller, - ViewSwitcher viewSwitcher, EventLoader eventLoader, int numDays) { - super(context); - mContext = context; - initAccessibilityVariables(); - - mResources = context.getResources(); - mNewEventHintString = mResources.getString(R.string.day_view_new_event_hint); - mNumDays = numDays; - - DATE_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.date_header_text_size); - DAY_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.day_label_text_size); - ONE_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.one_day_header_height); - DAY_HEADER_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.day_header_bottom_margin); - EXPAND_ALL_DAY_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.all_day_bottom_margin); - HOURS_TEXT_SIZE = (int) mResources.getDimension(R.dimen.hours_text_size); - AMPM_TEXT_SIZE = (int) mResources.getDimension(R.dimen.ampm_text_size); - MIN_HOURS_WIDTH = (int) mResources.getDimension(R.dimen.min_hours_width); - HOURS_LEFT_MARGIN = (int) mResources.getDimension(R.dimen.hours_left_margin); - HOURS_RIGHT_MARGIN = (int) mResources.getDimension(R.dimen.hours_right_margin); - MULTI_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.day_header_height); - int eventTextSizeId; - if (mNumDays == 1) { - eventTextSizeId = R.dimen.day_view_event_text_size; - } else { - eventTextSizeId = R.dimen.week_view_event_text_size; - } - EVENT_TEXT_FONT_SIZE = (int) mResources.getDimension(eventTextSizeId); - NEW_EVENT_HINT_FONT_SIZE = (int) mResources.getDimension(R.dimen.new_event_hint_text_size); - MIN_EVENT_HEIGHT = mResources.getDimension(R.dimen.event_min_height); - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = MIN_EVENT_HEIGHT; - EVENT_TEXT_TOP_MARGIN = (int) mResources.getDimension(R.dimen.event_text_vertical_margin); - EVENT_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; - EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; - EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; - - EVENT_TEXT_LEFT_MARGIN = (int) mResources - .getDimension(R.dimen.event_text_horizontal_margin); - EVENT_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; - EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; - - if (mScale == 0) { - - mScale = mResources.getDisplayMetrics().density; - if (mScale != 1) { - SINGLE_ALLDAY_HEIGHT *= mScale; - ALLDAY_TOP_MARGIN *= mScale; - MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale; - - NORMAL_FONT_SIZE *= mScale; - GRID_LINE_LEFT_MARGIN *= mScale; - HOURS_TOP_MARGIN *= mScale; - MIN_CELL_WIDTH_FOR_TEXT *= mScale; - MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale; - mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; - - CURRENT_TIME_LINE_SIDE_BUFFER *= mScale; - CURRENT_TIME_LINE_TOP_OFFSET *= mScale; - - MIN_Y_SPAN *= mScale; - MAX_CELL_HEIGHT *= mScale; - DEFAULT_CELL_HEIGHT *= mScale; - DAY_HEADER_HEIGHT *= mScale; - DAY_HEADER_RIGHT_MARGIN *= mScale; - DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale; - DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale; - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale; - CALENDAR_COLOR_SQUARE_SIZE *= mScale; - EVENT_RECT_TOP_MARGIN *= mScale; - EVENT_RECT_BOTTOM_MARGIN *= mScale; - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale; - EVENT_RECT_LEFT_MARGIN *= mScale; - EVENT_RECT_RIGHT_MARGIN *= mScale; - EVENT_RECT_STROKE_WIDTH *= mScale; - EVENT_SQUARE_WIDTH *= mScale; - EVENT_LINE_PADDING *= mScale; - NEW_EVENT_MARGIN *= mScale; - NEW_EVENT_WIDTH *= mScale; - NEW_EVENT_MAX_LENGTH *= mScale; - } - } - HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; - DAY_HEADER_HEIGHT = mNumDays == 1 ? ONE_DAY_HEADER_HEIGHT : MULTI_DAY_HEADER_HEIGHT; - - mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_indicator_holo_light); - mCurrentTimeAnimateLine = mResources - .getDrawable(R.drawable.timeline_indicator_activated_holo_light); - mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light); - mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_expand_holo_light); - mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_collapse_holo_light); - mNewEventHintColor = mResources.getColor(R.color.new_event_hint_text_color); - mAcceptedOrTentativeEventBoxDrawable = mResources - .getDrawable(R.drawable.panel_month_event_holo_light); - - mEventLoader = eventLoader; - mEventGeometry = new EventGeometry(); - mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT); - mEventGeometry.setHourGap(HOUR_GAP); - mEventGeometry.setCellMargin(DAY_GAP); - mLastPopupEventID = INVALID_EVENT_ID; - mController = controller; - mViewSwitcher = viewSwitcher; - mGestureDetector = new GestureDetector(context, new CalendarGestureListener()); - mScaleGestureDetector = new ScaleGestureDetector(getContext(), this); - if (mCellHeight == 0) { - mCellHeight = Utils.getSharedPreference(mContext, - GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT); - } - mScroller = new OverScroller(context); - mHScrollInterpolator = new ScrollInterpolator(); - mEdgeEffectTop = new EdgeEffect(context); - mEdgeEffectBottom = new EdgeEffect(context); - ViewConfiguration vc = ViewConfiguration.get(context); - mScaledPagingTouchSlop = vc.getScaledPagingTouchSlop(); - mOnDownDelay = ViewConfiguration.getTapTimeout(); - OVERFLING_DISTANCE = vc.getScaledOverflingDistance(); - - init(context); - } - - @Override - protected void onAttachedToWindow() { - if (mHandler == null) { - mHandler = getHandler(); - mHandler.post(mUpdateCurrentTime); - } - } - - private void init(Context context) { - setFocusable(true); - - // Allow focus in touch mode so that we can do keyboard shortcuts - // even after we've entered touch mode. - setFocusableInTouchMode(true); - setClickable(true); - setOnCreateContextMenuListener(this); - - mFirstDayOfWeek = Utils.getFirstDayOfWeek(context); - - mCurrentTime = new Time(Utils.getTimeZone(context, mTZUpdater)); - long currentTime = System.currentTimeMillis(); - mCurrentTime.set(currentTime); - mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); - - mWeek_saturdayColor = mResources.getColor(R.color.week_saturday); - mWeek_sundayColor = mResources.getColor(R.color.week_sunday); - mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color); - mFutureBgColorRes = mResources.getColor(R.color.calendar_future_bg_color); - mBgColor = mResources.getColor(R.color.calendar_hour_background); - mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label); - mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected); - mCalendarGridLineInnerHorizontalColor = mResources - .getColor(R.color.calendar_grid_line_inner_horizontal_color); - mCalendarGridLineInnerVerticalColor = mResources - .getColor(R.color.calendar_grid_line_inner_vertical_color); - mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label); - mEventTextColor = mResources.getColor(R.color.calendar_event_text_color); - mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color); - - mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE); - mEventTextPaint.setTextAlign(Paint.Align.LEFT); - mEventTextPaint.setAntiAlias(true); - - int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color); - Paint p = mSelectionPaint; - p.setColor(gridLineColor); - p.setStyle(Style.FILL); - p.setAntiAlias(false); - - p = mPaint; - p.setAntiAlias(true); - - // Allocate space for 2 weeks worth of weekday names so that we can - // easily start the week display at any week day. - mDayStrs = new String[14]; - - // Also create an array of 2-letter abbreviations. - mDayStrs2Letter = new String[14]; - - for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { - int index = i - Calendar.SUNDAY; - // e.g. Tue for Tuesday - mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM) - .toUpperCase(); - mDayStrs[index + 7] = mDayStrs[index]; - // e.g. Tu for Tuesday - mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT) - .toUpperCase(); - - // If we don't have 2-letter day strings, fall back to 1-letter. - if (mDayStrs2Letter[index].equals(mDayStrs[index])) { - mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST); - } - - mDayStrs2Letter[index + 7] = mDayStrs2Letter[index]; - } - - // Figure out how much space we need for the 3-letter abbrev names - // in the worst case. - p.setTextSize(DATE_HEADER_FONT_SIZE); - p.setTypeface(mBold); - String[] dateStrs = {" 28", " 30"}; - mDateStrWidth = computeMaxStringWidth(0, dateStrs, p); - p.setTextSize(DAY_HEADER_FONT_SIZE); - mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p); - - p.setTextSize(HOURS_TEXT_SIZE); - p.setTypeface(null); - handleOnResume(); - - mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase(); - mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase(); - String[] ampm = {mAmString, mPmString}; - p.setTextSize(AMPM_TEXT_SIZE); - mHoursWidth = Math.max(HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p) - + HOURS_RIGHT_MARGIN); - mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth); - - LayoutInflater inflater; - inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mPopupView = inflater.inflate(R.layout.bubble_event, null); - mPopupView.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - mPopup = new PopupWindow(context); - mPopup.setContentView(mPopupView); - Resources.Theme dialogTheme = getResources().newTheme(); - dialogTheme.applyStyle(android.R.style.Theme_Dialog, true); - TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] { - android.R.attr.windowBackground }); - mPopup.setBackgroundDrawable(ta.getDrawable(0)); - ta.recycle(); - - // Enable touching the popup window - mPopupView.setOnClickListener(this); - // Catch long clicks for creating a new event - setOnLongClickListener(this); - - mBaseDate = new Time(Utils.getTimeZone(context, mTZUpdater)); - long millis = System.currentTimeMillis(); - mBaseDate.set(millis); - - mEarliestStartHour = new int[mNumDays]; - mHasAllDayEvent = new boolean[mNumDays]; - - // mLines is the array of points used with Canvas.drawLines() in - // drawGridBackground() and drawAllDayEvents(). Its size depends - // on the max number of lines that can ever be drawn by any single - // drawLines() call in either of those methods. - final int maxGridLines = (24 + 1) // max horizontal lines we might draw - + (mNumDays + 1); // max vertical lines we might draw - mLines = new float[maxGridLines * 4]; - } - - /** - * This is called when the popup window is pressed. - */ - public void onClick(View v) { - if (v == mPopupView) { - // Pretend it was a trackball click because that will always - // jump to the "View event" screen. - switchViews(true /* trackball */); - } - } - - public void handleOnResume() { - initAccessibilityVariables(); - if(Utils.getSharedPreference(mContext, OtherPreferences.KEY_OTHER_1, false)) { - mFutureBgColor = 0; - } else { - mFutureBgColor = mFutureBgColorRes; - } - mIs24HourFormat = DateFormat.is24HourFormat(mContext); - mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm; - mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); - mLastSelectionDayForAccessibility = 0; - mLastSelectionHourForAccessibility = 0; - mLastSelectedEventForAccessibility = null; - mSelectionMode = SELECTION_HIDDEN; - } - - private void initAccessibilityVariables() { - mAccessibilityMgr = (AccessibilityManager) mContext - .getSystemService(Service.ACCESSIBILITY_SERVICE); - mIsAccessibilityEnabled = mAccessibilityMgr != null && mAccessibilityMgr.isEnabled(); - mTouchExplorationEnabled = isTouchExplorationEnabled(); - } - - /** - * Returns the start of the selected time in milliseconds since the epoch. - * - * @return selected time in UTC milliseconds since the epoch. - */ - long getSelectedTimeInMillis() { - Time time = new Time(mBaseDate); - time.setJulianDay(mSelectionDay); - time.hour = mSelectionHour; - - // We ignore the "isDst" field because we want normalize() to figure - // out the correct DST value and not adjust the selected time based - // on the current setting of DST. - return time.normalize(true /* ignore isDst */); - } - - Time getSelectedTime() { - Time time = new Time(mBaseDate); - time.setJulianDay(mSelectionDay); - time.hour = mSelectionHour; - - // We ignore the "isDst" field because we want normalize() to figure - // out the correct DST value and not adjust the selected time based - // on the current setting of DST. - time.normalize(true /* ignore isDst */); - return time; - } - - Time getSelectedTimeForAccessibility() { - Time time = new Time(mBaseDate); - time.setJulianDay(mSelectionDayForAccessibility); - time.hour = mSelectionHourForAccessibility; - - // We ignore the "isDst" field because we want normalize() to figure - // out the correct DST value and not adjust the selected time based - // on the current setting of DST. - time.normalize(true /* ignore isDst */); - return time; - } - - /** - * Returns the start of the selected time in minutes since midnight, - * local time. The derived class must ensure that this is consistent - * with the return value from getSelectedTimeInMillis(). - */ - int getSelectedMinutesSinceMidnight() { - return mSelectionHour * MINUTES_PER_HOUR; - } - - int getFirstVisibleHour() { - return mFirstHour; - } - - void setFirstVisibleHour(int firstHour) { - mFirstHour = firstHour; - mFirstHourOffset = 0; - } - - public void setSelected(Time time, boolean ignoreTime, boolean animateToday) { - mBaseDate.set(time); - setSelectedHour(mBaseDate.hour); - setSelectedEvent(null); - mPrevSelectedEvent = null; - long millis = mBaseDate.toMillis(false /* use isDst */); - setSelectedDay(Time.getJulianDay(millis, mBaseDate.gmtoff)); - mSelectedEvents.clear(); - mComputeSelectedEvents = true; - - int gotoY = Integer.MIN_VALUE; - - if (!ignoreTime && mGridAreaHeight != -1) { - int lastHour = 0; - - if (mBaseDate.hour < mFirstHour) { - // Above visible region - gotoY = mBaseDate.hour * (mCellHeight + HOUR_GAP); - } else { - lastHour = (mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP) - + mFirstHour; - - if (mBaseDate.hour >= lastHour) { - // Below visible region - - // target hour + 1 (to give it room to see the event) - - // grid height (to get the y of the top of the visible - // region) - gotoY = (int) ((mBaseDate.hour + 1 + mBaseDate.minute / 60.0f) - * (mCellHeight + HOUR_GAP) - mGridAreaHeight); - } - } - - if (DEBUG) { - Log.e(TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH " - + (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight - + " ymax " + mMaxViewStartY); - } - - if (gotoY > mMaxViewStartY) { - gotoY = mMaxViewStartY; - } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) { - gotoY = 0; - } - } - - recalc(); - - mRemeasure = true; - invalidate(); - - boolean delayAnimateToday = false; - if (gotoY != Integer.MIN_VALUE) { - ValueAnimator scrollAnim = ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY); - scrollAnim.setDuration(GOTO_SCROLL_DURATION); - scrollAnim.setInterpolator(new AccelerateDecelerateInterpolator()); - scrollAnim.addListener(mAnimatorListener); - scrollAnim.start(); - delayAnimateToday = true; - } - if (animateToday) { - synchronized (mTodayAnimatorListener) { - if (mTodayAnimator != null) { - mTodayAnimator.removeAllListeners(); - mTodayAnimator.cancel(); - } - mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha", - mAnimateTodayAlpha, 255); - mAnimateToday = true; - mTodayAnimatorListener.setFadingIn(true); - mTodayAnimatorListener.setAnimator(mTodayAnimator); - mTodayAnimator.addListener(mTodayAnimatorListener); - mTodayAnimator.setDuration(150); - if (delayAnimateToday) { - mTodayAnimator.setStartDelay(GOTO_SCROLL_DURATION); - } - mTodayAnimator.start(); - } - } - sendAccessibilityEventAsNeeded(false); - } - - // Called from animation framework via reflection. Do not remove - public void setViewStartY(int viewStartY) { - if (viewStartY > mMaxViewStartY) { - viewStartY = mMaxViewStartY; - } - - mViewStartY = viewStartY; - - computeFirstHour(); - invalidate(); - } - - public void setAnimateTodayAlpha(int todayAlpha) { - mAnimateTodayAlpha = todayAlpha; - invalidate(); - } - - public Time getSelectedDay() { - Time time = new Time(mBaseDate); - time.setJulianDay(mSelectionDay); - time.hour = mSelectionHour; - - // We ignore the "isDst" field because we want normalize() to figure - // out the correct DST value and not adjust the selected time based - // on the current setting of DST. - time.normalize(true /* ignore isDst */); - return time; - } - - public void updateTitle() { - Time start = new Time(mBaseDate); - start.normalize(true); - Time end = new Time(start); - end.monthDay += mNumDays - 1; - // Move it forward one minute so the formatter doesn't lose a day - end.minute += 1; - end.normalize(true); - - long formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; - if (mNumDays != 1) { - // Don't show day of the month if for multi-day view - formatFlags |= DateUtils.FORMAT_NO_MONTH_DAY; - - // Abbreviate the month if showing multiple months - if (start.month != end.month) { - formatFlags |= DateUtils.FORMAT_ABBREV_MONTH; - } - } - - mController.sendEvent(this, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT, - formatFlags, null, null); - } - - /** - * return a negative number if "time" is comes before the visible time - * range, a positive number if "time" is after the visible time range, and 0 - * if it is in the visible time range. - */ - public int compareToVisibleTimeRange(Time time) { - - int savedHour = mBaseDate.hour; - int savedMinute = mBaseDate.minute; - int savedSec = mBaseDate.second; - - mBaseDate.hour = 0; - mBaseDate.minute = 0; - mBaseDate.second = 0; - - if (DEBUG) { - Log.d(TAG, "Begin " + mBaseDate.toString()); - Log.d(TAG, "Diff " + time.toString()); - } - - // Compare beginning of range - int diff = Time.compare(time, mBaseDate); - if (diff > 0) { - // Compare end of range - mBaseDate.monthDay += mNumDays; - mBaseDate.normalize(true); - diff = Time.compare(time, mBaseDate); - - if (DEBUG) Log.d(TAG, "End " + mBaseDate.toString()); - - mBaseDate.monthDay -= mNumDays; - mBaseDate.normalize(true); - if (diff < 0) { - // in visible time - diff = 0; - } else if (diff == 0) { - // Midnight of following day - diff = 1; - } - } - - if (DEBUG) Log.d(TAG, "Diff: " + diff); - - mBaseDate.hour = savedHour; - mBaseDate.minute = savedMinute; - mBaseDate.second = savedSec; - return diff; - } - - private void recalc() { - // Set the base date to the beginning of the week if we are displaying - // 7 days at a time. - if (mNumDays == 7) { - adjustToBeginningOfWeek(mBaseDate); - } - - final long start = mBaseDate.toMillis(false /* use isDst */); - mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff); - mLastJulianDay = mFirstJulianDay + mNumDays - 1; - - mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY); - mFirstVisibleDate = mBaseDate.monthDay; - mFirstVisibleDayOfWeek = mBaseDate.weekDay; - } - - private void adjustToBeginningOfWeek(Time time) { - int dayOfWeek = time.weekDay; - int diff = dayOfWeek - mFirstDayOfWeek; - if (diff != 0) { - if (diff < 0) { - diff += 7; - } - time.monthDay -= diff; - time.normalize(true /* ignore isDst */); - } - } - - @Override - protected void onSizeChanged(int width, int height, int oldw, int oldh) { - mViewWidth = width; - mViewHeight = height; - mEdgeEffectTop.setSize(mViewWidth, mViewHeight); - mEdgeEffectBottom.setSize(mViewWidth, mViewHeight); - int gridAreaWidth = width - mHoursWidth; - mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays; - - // This would be about 1 day worth in a 7 day view - mHorizontalSnapBackThreshold = width / 7; - - Paint p = new Paint(); - p.setTextSize(HOURS_TEXT_SIZE); - mHoursTextHeight = (int) Math.abs(p.ascent()); - remeasure(width, height); - } - - /** - * Measures the space needed for various parts of the view after - * loading new events. This can change if there are all-day events. - */ - private void remeasure(int width, int height) { - // Shrink to fit available space but make sure we can display at least two events - MAX_UNEXPANDED_ALLDAY_HEIGHT = (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); - MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6); - MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(MAX_UNEXPANDED_ALLDAY_HEIGHT, - (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 2); - mMaxUnexpandedAlldayEventCount = - (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); - - // First, clear the array of earliest start times, and the array - // indicating presence of an all-day event. - for (int day = 0; day < mNumDays; day++) { - mEarliestStartHour[day] = 25; // some big number - mHasAllDayEvent[day] = false; - } - - int maxAllDayEvents = mMaxAlldayEvents; - - // The min is where 24 hours cover the entire visible area - mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, (int) MIN_EVENT_HEIGHT); - if (mCellHeight < mMinCellHeight) { - mCellHeight = mMinCellHeight; - } - - // Calculate mAllDayHeight - mFirstCell = DAY_HEADER_HEIGHT; - int allDayHeight = 0; - if (maxAllDayEvents > 0) { - int maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; - // If there is at most one all-day event per day, then use less - // space (but more than the space for a single event). - if (maxAllDayEvents == 1) { - allDayHeight = SINGLE_ALLDAY_HEIGHT; - } else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount){ - // Allow the all-day area to grow in height depending on the - // number of all-day events we need to show, up to a limit. - allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; - if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { - allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT; - } - } else { - // if we have more than the magic number, check if we're animating - // and if not adjust the sizes appropriately - if (mAnimateDayHeight != 0) { - // Don't shrink the space past the final allDay space. The animation - // continues to hide the last event so the more events text can - // fade in. - allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT); - } else { - // Try to fit all the events in - allDayHeight = (int) (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); - // But clip the area depending on which mode we're in - if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { - allDayHeight = (int) (mMaxUnexpandedAlldayEventCount * - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); - } else if (allDayHeight > maxAllAllDayHeight) { - allDayHeight = maxAllAllDayHeight; - } - } - } - mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN; - } else { - mSelectionAllday = false; - } - mAlldayHeight = allDayHeight; - - mGridAreaHeight = height - mFirstCell; - - // Set up the expand icon position - int allDayIconWidth = mExpandAlldayDrawable.getIntrinsicWidth(); - mExpandAllDayRect.left = Math.max((mHoursWidth - allDayIconWidth) / 2, - EVENT_ALL_DAY_TEXT_LEFT_MARGIN); - mExpandAllDayRect.right = Math.min(mExpandAllDayRect.left + allDayIconWidth, mHoursWidth - - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN); - mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN; - mExpandAllDayRect.top = mExpandAllDayRect.bottom - - mExpandAlldayDrawable.getIntrinsicHeight(); - - mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP); - mEventGeometry.setHourHeight(mCellHeight); - - final long minimumDurationMillis = (long) - (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)); - Event.computePositions(mEvents, minimumDurationMillis); - - // Compute the top of our reachable view - mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; - if (DEBUG) { - Log.e(TAG, "mViewStartY: " + mViewStartY); - Log.e(TAG, "mMaxViewStartY: " + mMaxViewStartY); - } - if (mViewStartY > mMaxViewStartY) { - mViewStartY = mMaxViewStartY; - computeFirstHour(); - } - - if (mFirstHour == -1) { - initFirstHour(); - mFirstHourOffset = 0; - } - - // When we change the base date, the number of all-day events may - // change and that changes the cell height. When we switch dates, - // we use the mFirstHourOffset from the previous view, but that may - // be too large for the new view if the cell height is smaller. - if (mFirstHourOffset >= mCellHeight + HOUR_GAP) { - mFirstHourOffset = mCellHeight + HOUR_GAP - 1; - } - mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset; - - final int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP); - //When we get new events we don't want to dismiss the popup unless the event changes - if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) { - mPopup.dismiss(); - } - mPopup.setWidth(eventAreaWidth - 20); - mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); - } - - /** - * Initialize the state for another view. The given view is one that has - * its own bitmap and will use an animation to replace the current view. - * The current view and new view are either both Week views or both Day - * views. They differ in their base date. - * - * @param view the view to initialize. - */ - private void initView(DayView view) { - view.setSelectedHour(mSelectionHour); - view.mSelectedEvents.clear(); - view.mComputeSelectedEvents = true; - view.mFirstHour = mFirstHour; - view.mFirstHourOffset = mFirstHourOffset; - view.remeasure(getWidth(), getHeight()); - view.initAllDayHeights(); - - view.setSelectedEvent(null); - view.mPrevSelectedEvent = null; - view.mFirstDayOfWeek = mFirstDayOfWeek; - if (view.mEvents.size() > 0) { - view.mSelectionAllday = mSelectionAllday; - } else { - view.mSelectionAllday = false; - } - - // Redraw the screen so that the selection box will be redrawn. We may - // have scrolled to a different part of the day in some other view - // so the selection box in this view may no longer be visible. - view.recalc(); - } - - /** - * Switch to another view based on what was selected (an event or a free - * slot) and how it was selected (by touch or by trackball). - * - * @param trackBallSelection true if the selection was made using the - * trackball. - */ - private void switchViews(boolean trackBallSelection) { - Event selectedEvent = mSelectedEvent; - - mPopup.dismiss(); - mLastPopupEventID = INVALID_EVENT_ID; - if (mNumDays > 1) { - // This is the Week view. - // With touch, we always switch to Day/Agenda View - // With track ball, if we selected a free slot, then create an event. - // If we selected a specific event, switch to EventInfo view. - if (trackBallSelection) { - if (selectedEvent != null) { - if (mIsAccessibilityEnabled) { - mAccessibilityMgr.interrupt(); - } - } - } - } - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - mScrolling = false; - return super.onKeyUp(keyCode, event); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return super.onKeyDown(keyCode, event); - } - - - @Override - public boolean onHoverEvent(MotionEvent event) { - return true; - } - - private boolean isTouchExplorationEnabled() { - return mIsAccessibilityEnabled && mAccessibilityMgr.isTouchExplorationEnabled(); - } - - private void sendAccessibilityEventAsNeeded(boolean speakEvents) { - if (!mIsAccessibilityEnabled) { - return; - } - boolean dayChanged = mLastSelectionDayForAccessibility != mSelectionDayForAccessibility; - boolean hourChanged = mLastSelectionHourForAccessibility != mSelectionHourForAccessibility; - if (dayChanged || hourChanged || - mLastSelectedEventForAccessibility != mSelectedEventForAccessibility) { - mLastSelectionDayForAccessibility = mSelectionDayForAccessibility; - mLastSelectionHourForAccessibility = mSelectionHourForAccessibility; - mLastSelectedEventForAccessibility = mSelectedEventForAccessibility; - - StringBuilder b = new StringBuilder(); - - // Announce only the changes i.e. day or hour or both - if (dayChanged) { - b.append(getSelectedTimeForAccessibility().format("%A ")); - } - if (hourChanged) { - b.append(getSelectedTimeForAccessibility().format(mIs24HourFormat ? "%k" : "%l%p")); - } - if (dayChanged || hourChanged) { - b.append(PERIOD_SPACE); - } - - if (speakEvents) { - if (mEventCountTemplate == null) { - mEventCountTemplate = mContext.getString(R.string.template_announce_item_index); - } - - // Read out the relevant event(s) - int numEvents = mSelectedEvents.size(); - if (numEvents > 0) { - if (mSelectedEventForAccessibility == null) { - // Read out all the events - int i = 1; - for (Event calEvent : mSelectedEvents) { - if (numEvents > 1) { - // Read out x of numEvents if there are more than one event - mStringBuilder.setLength(0); - b.append(mFormatter.format(mEventCountTemplate, i++, numEvents)); - b.append(" "); - } - appendEventAccessibilityString(b, calEvent); - } - } else { - if (numEvents > 1) { - // Read out x of numEvents if there are more than one event - mStringBuilder.setLength(0); - b.append(mFormatter.format(mEventCountTemplate, mSelectedEvents - .indexOf(mSelectedEventForAccessibility) + 1, numEvents)); - b.append(" "); - } - appendEventAccessibilityString(b, mSelectedEventForAccessibility); - } - } - } - - if (dayChanged || hourChanged || speakEvents) { - AccessibilityEvent event = AccessibilityEvent - .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); - CharSequence msg = b.toString(); - event.getText().add(msg); - event.setAddedCount(msg.length()); - sendAccessibilityEventUnchecked(event); - } - } - } - - /** - * @param b - * @param calEvent - */ - private void appendEventAccessibilityString(StringBuilder b, Event calEvent) { - b.append(calEvent.getTitleAndLocation()); - b.append(PERIOD_SPACE); - String when; - int flags = DateUtils.FORMAT_SHOW_DATE; - if (calEvent.allDay) { - flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY; - } else { - flags |= DateUtils.FORMAT_SHOW_TIME; - if (DateFormat.is24HourFormat(mContext)) { - flags |= DateUtils.FORMAT_24HOUR; - } - } - when = Utils.formatDateRange(mContext, calEvent.startMillis, calEvent.endMillis, flags); - b.append(when); - b.append(PERIOD_SPACE); - } - - private class GotoBroadcaster implements Animation.AnimationListener { - private final int mCounter; - private final Time mStart; - private final Time mEnd; - - public GotoBroadcaster(Time start, Time end) { - mCounter = ++sCounter; - mStart = start; - mEnd = end; - } - - @Override - public void onAnimationEnd(Animation animation) { - DayView view = (DayView) mViewSwitcher.getCurrentView(); - view.mViewStartX = 0; - view = (DayView) mViewSwitcher.getNextView(); - view.mViewStartX = 0; - - if (mCounter == sCounter) { - mController.sendEvent(this, EventType.GO_TO, mStart, mEnd, null, -1, - ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); - } - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - - @Override - public void onAnimationStart(Animation animation) { - } - } - - private View switchViews(boolean forward, float xOffSet, float width, float velocity) { - mAnimationDistance = width - xOffSet; - if (DEBUG) { - Log.d(TAG, "switchViews(" + forward + ") O:" + xOffSet + " Dist:" + mAnimationDistance); - } - - float progress = Math.abs(xOffSet) / width; - if (progress > 1.0f) { - progress = 1.0f; - } - - float inFromXValue, inToXValue; - float outFromXValue, outToXValue; - if (forward) { - inFromXValue = 1.0f - progress; - inToXValue = 0.0f; - outFromXValue = -progress; - outToXValue = -1.0f; - } else { - inFromXValue = progress - 1.0f; - inToXValue = 0.0f; - outFromXValue = progress; - outToXValue = 1.0f; - } - - final Time start = new Time(mBaseDate.timezone); - start.set(mController.getTime()); - if (forward) { - start.monthDay += mNumDays; - } else { - start.monthDay -= mNumDays; - } - mController.setTime(start.normalize(true)); - - Time newSelected = start; - - if (mNumDays == 7) { - newSelected = new Time(start); - adjustToBeginningOfWeek(start); - } - - final Time end = new Time(start); - end.monthDay += mNumDays - 1; - - // We have to allocate these animation objects each time we switch views - // because that is the only way to set the animation parameters. - TranslateAnimation inAnimation = new TranslateAnimation( - Animation.RELATIVE_TO_SELF, inFromXValue, - Animation.RELATIVE_TO_SELF, inToXValue, - Animation.ABSOLUTE, 0.0f, - Animation.ABSOLUTE, 0.0f); - - TranslateAnimation outAnimation = new TranslateAnimation( - Animation.RELATIVE_TO_SELF, outFromXValue, - Animation.RELATIVE_TO_SELF, outToXValue, - Animation.ABSOLUTE, 0.0f, - Animation.ABSOLUTE, 0.0f); - - long duration = calculateDuration(width - Math.abs(xOffSet), width, velocity); - inAnimation.setDuration(duration); - inAnimation.setInterpolator(mHScrollInterpolator); - outAnimation.setInterpolator(mHScrollInterpolator); - outAnimation.setDuration(duration); - outAnimation.setAnimationListener(new GotoBroadcaster(start, end)); - mViewSwitcher.setInAnimation(inAnimation); - mViewSwitcher.setOutAnimation(outAnimation); - - DayView view = (DayView) mViewSwitcher.getCurrentView(); - view.cleanup(); - mViewSwitcher.showNext(); - view = (DayView) mViewSwitcher.getCurrentView(); - view.setSelected(newSelected, true, false); - view.requestFocus(); - view.reloadEvents(); - view.updateTitle(); - view.restartCurrentTimeUpdates(); - - return view; - } - - // This is called after scrolling stops to move the selected hour - // to the visible part of the screen. - private void resetSelectedHour() { - if (mSelectionHour < mFirstHour + 1) { - setSelectedHour(mFirstHour + 1); - setSelectedEvent(null); - mSelectedEvents.clear(); - mComputeSelectedEvents = true; - } else if (mSelectionHour > mFirstHour + mNumHours - 3) { - setSelectedHour(mFirstHour + mNumHours - 3); - setSelectedEvent(null); - mSelectedEvents.clear(); - mComputeSelectedEvents = true; - } - } - - private void initFirstHour() { - mFirstHour = mSelectionHour - mNumHours / 5; - if (mFirstHour < 0) { - mFirstHour = 0; - } else if (mFirstHour + mNumHours > 24) { - mFirstHour = 24 - mNumHours; - } - } - - /** - * Recomputes the first full hour that is visible on screen after the - * screen is scrolled. - */ - private void computeFirstHour() { - // Compute the first full hour that is visible on screen - mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP); - mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY; - } - - private void adjustHourSelection() { - if (mSelectionHour < 0) { - setSelectedHour(0); - if (mMaxAlldayEvents > 0) { - mPrevSelectedEvent = null; - mSelectionAllday = true; - } - } - - if (mSelectionHour > 23) { - setSelectedHour(23); - } - - // If the selected hour is at least 2 time slots from the top and - // bottom of the screen, then don't scroll the view. - if (mSelectionHour < mFirstHour + 1) { - // If there are all-days events for the selected day but there - // are no more normal events earlier in the day, then jump to - // the all-day event area. - // Exception 1: allow the user to scroll to 8am with the trackball - // before jumping to the all-day event area. - // Exception 2: if 12am is on screen, then allow the user to select - // 12am before going up to the all-day event area. - int daynum = mSelectionDay - mFirstJulianDay; - if (daynum < mEarliestStartHour.length && daynum >= 0 - && mMaxAlldayEvents > 0 - && mEarliestStartHour[daynum] > mSelectionHour - && mFirstHour > 0 && mFirstHour < 8) { - mPrevSelectedEvent = null; - mSelectionAllday = true; - setSelectedHour(mFirstHour + 1); - return; - } - - if (mFirstHour > 0) { - mFirstHour -= 1; - mViewStartY -= (mCellHeight + HOUR_GAP); - if (mViewStartY < 0) { - mViewStartY = 0; - } - return; - } - } - - if (mSelectionHour > mFirstHour + mNumHours - 3) { - if (mFirstHour < 24 - mNumHours) { - mFirstHour += 1; - mViewStartY += (mCellHeight + HOUR_GAP); - if (mViewStartY > mMaxViewStartY) { - mViewStartY = mMaxViewStartY; - } - return; - } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { - mViewStartY = mMaxViewStartY; - } - } - } - - void clearCachedEvents() { - mLastReloadMillis = 0; - } - - private final Runnable mCancelCallback = new Runnable() { - public void run() { - clearCachedEvents(); - } - }; - - /* package */ void reloadEvents() { - // Protect against this being called before this view has been - // initialized. -// if (mContext == null) { -// return; -// } - - // Make sure our time zones are up to date - mTZUpdater.run(); - - setSelectedEvent(null); - mPrevSelectedEvent = null; - mSelectedEvents.clear(); - - // The start date is the beginning of the week at 12am - Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater)); - weekStart.set(mBaseDate); - weekStart.hour = 0; - weekStart.minute = 0; - weekStart.second = 0; - long millis = weekStart.normalize(true /* ignore isDst */); - - // Avoid reloading events unnecessarily. - if (millis == mLastReloadMillis) { - return; - } - mLastReloadMillis = millis; - - // load events in the background -// mContext.startProgressSpinner(); - final ArrayList<Event> events = new ArrayList<Event>(); - mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() { - - public void run() { - boolean fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay; - mEvents = events; - mLoadedFirstJulianDay = mFirstJulianDay; - if (mAllDayEvents == null) { - mAllDayEvents = new ArrayList<Event>(); - } else { - mAllDayEvents.clear(); - } - - // Create a shorter array for all day events - for (Event e : events) { - if (e.drawAsAllday()) { - mAllDayEvents.add(e); - } - } - - // New events, new layouts - if (mLayouts == null || mLayouts.length < events.size()) { - mLayouts = new StaticLayout[events.size()]; - } else { - Arrays.fill(mLayouts, null); - } - - if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) { - mAllDayLayouts = new StaticLayout[events.size()]; - } else { - Arrays.fill(mAllDayLayouts, null); - } - - computeEventRelations(); - - mRemeasure = true; - mComputeSelectedEvents = true; - recalc(); - - // Start animation to cross fade the events - if (fadeinEvents) { - if (mEventsCrossFadeAnimation == null) { - mEventsCrossFadeAnimation = - ObjectAnimator.ofInt(DayView.this, "EventsAlpha", 0, 255); - mEventsCrossFadeAnimation.setDuration(EVENTS_CROSS_FADE_DURATION); - } - mEventsCrossFadeAnimation.start(); - } else{ - invalidate(); - } - } - }, mCancelCallback); - } - - public void setEventsAlpha(int alpha) { - mEventsAlpha = alpha; - invalidate(); - } - - public int getEventsAlpha() { - return mEventsAlpha; - } - - public void stopEventsAnimation() { - if (mEventsCrossFadeAnimation != null) { - mEventsCrossFadeAnimation.cancel(); - } - mEventsAlpha = 255; - } - - private void computeEventRelations() { - // Compute the layout relation between each event before measuring cell - // width, as the cell width should be adjusted along with the relation. - // - // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm) - // We should mark them as "overwapped". Though they are not overwapped logically, but - // minimum cell height implicitly expands the cell height of A and it should look like - // (1:00pm - 1:15pm) after the cell height adjustment. - - // Compute the space needed for the all-day events, if any. - // Make a pass over all the events, and keep track of the maximum - // number of all-day events in any one day. Also, keep track of - // the earliest event in each day. - int maxAllDayEvents = 0; - final ArrayList<Event> events = mEvents; - final int len = events.size(); - // Num of all-day-events on each day. - final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1]; - Arrays.fill(eventsCount, 0); - for (int ii = 0; ii < len; ii++) { - Event event = events.get(ii); - if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) { - continue; - } - if (event.drawAsAllday()) { - // Count all the events being drawn as allDay events - final int firstDay = Math.max(event.startDay, mFirstJulianDay); - final int lastDay = Math.min(event.endDay, mLastJulianDay); - for (int day = firstDay; day <= lastDay; day++) { - final int count = ++eventsCount[day - mFirstJulianDay]; - if (maxAllDayEvents < count) { - maxAllDayEvents = count; - } - } - - int daynum = event.startDay - mFirstJulianDay; - int durationDays = event.endDay - event.startDay + 1; - if (daynum < 0) { - durationDays += daynum; - daynum = 0; - } - if (daynum + durationDays > mNumDays) { - durationDays = mNumDays - daynum; - } - for (int day = daynum; durationDays > 0; day++, durationDays--) { - mHasAllDayEvent[day] = true; - } - } else { - int daynum = event.startDay - mFirstJulianDay; - int hour = event.startTime / 60; - if (daynum >= 0 && hour < mEarliestStartHour[daynum]) { - mEarliestStartHour[daynum] = hour; - } - - // Also check the end hour in case the event spans more than - // one day. - daynum = event.endDay - mFirstJulianDay; - hour = event.endTime / 60; - if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) { - mEarliestStartHour[daynum] = hour; - } - } - } - mMaxAlldayEvents = maxAllDayEvents; - initAllDayHeights(); - } - - @Override - protected void onDraw(Canvas canvas) { - if (mRemeasure) { - remeasure(getWidth(), getHeight()); - mRemeasure = false; - } - canvas.save(); - - float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight; - // offset canvas by the current drag and header position - canvas.translate(-mViewStartX, yTranslate); - // clip to everything below the allDay area - Rect dest = mDestRect; - dest.top = (int) (mFirstCell - yTranslate); - dest.bottom = (int) (mViewHeight - yTranslate); - dest.left = 0; - dest.right = mViewWidth; - canvas.save(); - canvas.clipRect(dest); - // Draw the movable part of the view - doDraw(canvas); - // restore to having no clip - canvas.restore(); - - if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { - float xTranslate; - if (mViewStartX > 0) { - xTranslate = mViewWidth; - } else { - xTranslate = -mViewWidth; - } - // Move the canvas around to prep it for the next view - // specifically, shift it by a screen and undo the - // yTranslation which will be redone in the nextView's onDraw(). - canvas.translate(xTranslate, -yTranslate); - DayView nextView = (DayView) mViewSwitcher.getNextView(); - - // Prevent infinite recursive calls to onDraw(). - nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE; - - nextView.onDraw(canvas); - // Move it back for this view - canvas.translate(-xTranslate, 0); - } else { - // If we drew another view we already translated it back - // If we didn't draw another view we should be at the edge of the - // screen - canvas.translate(mViewStartX, -yTranslate); - } - - // Draw the fixed areas (that don't scroll) directly to the canvas. - drawAfterScroll(canvas); - if (mComputeSelectedEvents && mUpdateToast) { - mUpdateToast = false; - } - mComputeSelectedEvents = false; - - // Draw overscroll glow - if (!mEdgeEffectTop.isFinished()) { - if (DAY_HEADER_HEIGHT != 0) { - canvas.translate(0, DAY_HEADER_HEIGHT); - } - if (mEdgeEffectTop.draw(canvas)) { - invalidate(); - } - if (DAY_HEADER_HEIGHT != 0) { - canvas.translate(0, -DAY_HEADER_HEIGHT); - } - } - if (!mEdgeEffectBottom.isFinished()) { - canvas.rotate(180, mViewWidth/2, mViewHeight/2); - if (mEdgeEffectBottom.draw(canvas)) { - invalidate(); - } - } - canvas.restore(); - } - - private void drawAfterScroll(Canvas canvas) { - Paint p = mPaint; - Rect r = mRect; - - drawAllDayHighlights(r, canvas, p); - if (mMaxAlldayEvents != 0) { - drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p); - drawUpperLeftCorner(r, canvas, p); - } - - drawScrollLine(r, canvas, p); - drawDayHeaderLoop(r, canvas, p); - - // Draw the AM and PM indicators if we're in 12 hour mode - if (!mIs24HourFormat) { - drawAmPm(canvas, p); - } - } - - // This isn't really the upper-left corner. It's the square area just - // below the upper-left corner, above the hours and to the left of the - // all-day area. - private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) { - setupHourTextPaint(p); - if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { - // Draw the allDay expand/collapse icon - if (mUseExpandIcon) { - mExpandAlldayDrawable.setBounds(mExpandAllDayRect); - mExpandAlldayDrawable.draw(canvas); - } else { - mCollapseAlldayDrawable.setBounds(mExpandAllDayRect); - mCollapseAlldayDrawable.draw(canvas); - } - } - } - - private void drawScrollLine(Rect r, Canvas canvas, Paint p) { - final int right = computeDayLeftPosition(mNumDays); - final int y = mFirstCell - 1; - - p.setAntiAlias(false); - p.setStyle(Style.FILL); - - p.setColor(mCalendarGridLineInnerHorizontalColor); - p.setStrokeWidth(GRID_LINE_INNER_WIDTH); - canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p); - p.setAntiAlias(true); - } - - // Computes the x position for the left side of the given day (base 0) - private int computeDayLeftPosition(int day) { - int effectiveWidth = mViewWidth - mHoursWidth; - return day * effectiveWidth / mNumDays + mHoursWidth; - } - - private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) { - if (mFutureBgColor != 0) { - // First, color the labels area light gray - r.top = 0; - r.bottom = DAY_HEADER_HEIGHT; - r.left = 0; - r.right = mViewWidth; - p.setColor(mBgColor); - p.setStyle(Style.FILL); - canvas.drawRect(r, p); - // and the area that says All day - r.top = DAY_HEADER_HEIGHT; - r.bottom = mFirstCell - 1; - r.left = 0; - r.right = mHoursWidth; - canvas.drawRect(r, p); - - int startIndex = -1; - - int todayIndex = mTodayJulianDay - mFirstJulianDay; - if (todayIndex < 0) { - // Future - startIndex = 0; - } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) { - // Multiday - tomorrow is visible. - startIndex = todayIndex + 1; - } - - if (startIndex >= 0) { - // Draw the future highlight - r.top = 0; - r.bottom = mFirstCell - 1; - r.left = computeDayLeftPosition(startIndex) + 1; - r.right = computeDayLeftPosition(mNumDays); - p.setColor(mFutureBgColor); - p.setStyle(Style.FILL); - canvas.drawRect(r, p); - } - } - } - - private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) { - // Draw the horizontal day background banner - // p.setColor(mCalendarDateBannerBackground); - // r.top = 0; - // r.bottom = DAY_HEADER_HEIGHT; - // r.left = 0; - // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP); - // canvas.drawRect(r, p); - // - // Fill the extra space on the right side with the default background - // r.left = r.right; - // r.right = mViewWidth; - // p.setColor(mCalendarGridAreaBackground); - // canvas.drawRect(r, p); - if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) { - return; - } - - p.setTypeface(mBold); - p.setTextAlign(Paint.Align.RIGHT); - int cell = mFirstJulianDay; - - String[] dayNames; - if (mDateStrWidth < mCellWidth) { - dayNames = mDayStrs; - } else { - dayNames = mDayStrs2Letter; - } - - p.setAntiAlias(true); - for (int day = 0; day < mNumDays; day++, cell++) { - int dayOfWeek = day + mFirstVisibleDayOfWeek; - if (dayOfWeek >= 14) { - dayOfWeek -= 14; - } - - int color = mCalendarDateBannerTextColor; - if (mNumDays == 1) { - if (dayOfWeek == Time.SATURDAY) { - color = mWeek_saturdayColor; - } else if (dayOfWeek == Time.SUNDAY) { - color = mWeek_sundayColor; - } - } else { - final int column = day % 7; - if (Utils.isSaturday(column, mFirstDayOfWeek)) { - color = mWeek_saturdayColor; - } else if (Utils.isSunday(column, mFirstDayOfWeek)) { - color = mWeek_sundayColor; - } - } - - p.setColor(color); - drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p); - } - p.setTypeface(null); - } - - private void drawAmPm(Canvas canvas, Paint p) { - p.setColor(mCalendarAmPmLabel); - p.setTextSize(AMPM_TEXT_SIZE); - p.setTypeface(mBold); - p.setAntiAlias(true); - p.setTextAlign(Paint.Align.RIGHT); - String text = mAmString; - if (mFirstHour >= 12) { - text = mPmString; - } - int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP; - canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); - - if (mFirstHour < 12 && mFirstHour + mNumHours > 12) { - // Also draw the "PM" - text = mPmString; - y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) - + 2 * mHoursTextHeight + HOUR_GAP; - canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); - } - } - - private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas, - Paint p) { - r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1; - r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1; - - r.top = top - CURRENT_TIME_LINE_TOP_OFFSET; - r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight(); - - mCurrentTimeLine.setBounds(r); - mCurrentTimeLine.draw(canvas); - if (mAnimateToday) { - mCurrentTimeAnimateLine.setBounds(r); - mCurrentTimeAnimateLine.setAlpha(mAnimateTodayAlpha); - mCurrentTimeAnimateLine.draw(canvas); - } - } - - private void doDraw(Canvas canvas) { - Paint p = mPaint; - Rect r = mRect; - - if (mFutureBgColor != 0) { - drawBgColors(r, canvas, p); - } - drawGridBackground(r, canvas, p); - drawHours(r, canvas, p); - - // Draw each day - int cell = mFirstJulianDay; - p.setAntiAlias(false); - int alpha = p.getAlpha(); - p.setAlpha(mEventsAlpha); - for (int day = 0; day < mNumDays; day++, cell++) { - // TODO Wow, this needs cleanup. drawEvents loop through all the - // events on every call. - drawEvents(cell, day, HOUR_GAP, canvas, p); - // If this is today - if (cell == mTodayJulianDay) { - int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) - + ((mCurrentTime.minute * mCellHeight) / 60) + 1; - - // And the current time shows up somewhere on the screen - if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { - drawCurrentTimeLine(r, day, lineY, canvas, p); - } - } - } - p.setAntiAlias(true); - p.setAlpha(alpha); - } - - private void drawHours(Rect r, Canvas canvas, Paint p) { - setupHourTextPaint(p); - - int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN; - - for (int i = 0; i < 24; i++) { - String time = mHourStrs[i]; - canvas.drawText(time, HOURS_LEFT_MARGIN, y, p); - y += mCellHeight + HOUR_GAP; - } - } - - private void setupHourTextPaint(Paint p) { - p.setColor(mCalendarHourLabelColor); - p.setTextSize(HOURS_TEXT_SIZE); - p.setTypeface(Typeface.DEFAULT); - p.setTextAlign(Paint.Align.RIGHT); - p.setAntiAlias(true); - } - - private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) { - int dateNum = mFirstVisibleDate + day; - int x; - if (dateNum > mMonthLength) { - dateNum -= mMonthLength; - } - p.setAntiAlias(true); - - int todayIndex = mTodayJulianDay - mFirstJulianDay; - // Draw day of the month - String dateNumStr = String.valueOf(dateNum); - if (mNumDays > 1) { - float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN; - - // Draw day of the month - x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN; - p.setTextAlign(Align.RIGHT); - p.setTextSize(DATE_HEADER_FONT_SIZE); - - p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); - canvas.drawText(dateNumStr, x, y, p); - - // Draw day of the week - x -= p.measureText(" " + dateNumStr); - p.setTextSize(DAY_HEADER_FONT_SIZE); - p.setTypeface(Typeface.DEFAULT); - canvas.drawText(dayStr, x, y, p); - } else { - float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN; - p.setTextAlign(Align.LEFT); - - - // Draw day of the week - x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN; - p.setTextSize(DAY_HEADER_FONT_SIZE); - p.setTypeface(Typeface.DEFAULT); - canvas.drawText(dayStr, x, y, p); - - // Draw day of the month - x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN; - p.setTextSize(DATE_HEADER_FONT_SIZE); - p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); - canvas.drawText(dateNumStr, x, y, p); - } - } - - private void drawGridBackground(Rect r, Canvas canvas, Paint p) { - Paint.Style savedStyle = p.getStyle(); - - final float stopX = computeDayLeftPosition(mNumDays); - float y = 0; - final float deltaY = mCellHeight + HOUR_GAP; - int linesIndex = 0; - final float startY = 0; - final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP); - float x = mHoursWidth; - - // Draw the inner horizontal grid lines - p.setColor(mCalendarGridLineInnerHorizontalColor); - p.setStrokeWidth(GRID_LINE_INNER_WIDTH); - p.setAntiAlias(false); - y = 0; - linesIndex = 0; - for (int hour = 0; hour <= 24; hour++) { - mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; - mLines[linesIndex++] = y; - mLines[linesIndex++] = stopX; - mLines[linesIndex++] = y; - y += deltaY; - } - if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) { - canvas.drawLines(mLines, 0, linesIndex, p); - linesIndex = 0; - p.setColor(mCalendarGridLineInnerVerticalColor); - } - - // Draw the inner vertical grid lines - for (int day = 0; day <= mNumDays; day++) { - x = computeDayLeftPosition(day); - mLines[linesIndex++] = x; - mLines[linesIndex++] = startY; - mLines[linesIndex++] = x; - mLines[linesIndex++] = stopY; - } - canvas.drawLines(mLines, 0, linesIndex, p); - - // Restore the saved style. - p.setStyle(savedStyle); - p.setAntiAlias(true); - } - - /** - * @param r - * @param canvas - * @param p - */ - private void drawBgColors(Rect r, Canvas canvas, Paint p) { - int todayIndex = mTodayJulianDay - mFirstJulianDay; - // Draw the hours background color - r.top = mDestRect.top; - r.bottom = mDestRect.bottom; - r.left = 0; - r.right = mHoursWidth; - p.setColor(mBgColor); - p.setStyle(Style.FILL); - p.setAntiAlias(false); - canvas.drawRect(r, p); - - // Draw background for grid area - if (mNumDays == 1 && todayIndex == 0) { - // Draw a white background for the time later than current time - int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) - + ((mCurrentTime.minute * mCellHeight) / 60) + 1; - if (lineY < mViewStartY + mViewHeight) { - lineY = Math.max(lineY, mViewStartY); - r.left = mHoursWidth; - r.right = mViewWidth; - r.top = lineY; - r.bottom = mViewStartY + mViewHeight; - p.setColor(mFutureBgColor); - canvas.drawRect(r, p); - } - } else if (todayIndex >= 0 && todayIndex < mNumDays) { - // Draw today with a white background for the time later than current time - int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) - + ((mCurrentTime.minute * mCellHeight) / 60) + 1; - if (lineY < mViewStartY + mViewHeight) { - lineY = Math.max(lineY, mViewStartY); - r.left = computeDayLeftPosition(todayIndex) + 1; - r.right = computeDayLeftPosition(todayIndex + 1); - r.top = lineY; - r.bottom = mViewStartY + mViewHeight; - p.setColor(mFutureBgColor); - canvas.drawRect(r, p); - } - - // Paint Tomorrow and later days with future color - if (todayIndex + 1 < mNumDays) { - r.left = computeDayLeftPosition(todayIndex + 1) + 1; - r.right = computeDayLeftPosition(mNumDays); - r.top = mDestRect.top; - r.bottom = mDestRect.bottom; - p.setColor(mFutureBgColor); - canvas.drawRect(r, p); - } - } else if (todayIndex < 0) { - // Future - r.left = computeDayLeftPosition(0) + 1; - r.right = computeDayLeftPosition(mNumDays); - r.top = mDestRect.top; - r.bottom = mDestRect.bottom; - p.setColor(mFutureBgColor); - canvas.drawRect(r, p); - } - p.setAntiAlias(true); - } - - private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) { - float maxWidthF = 0.0f; - - int len = strings.length; - for (int i = 0; i < len; i++) { - float width = p.measureText(strings[i]); - maxWidthF = Math.max(width, maxWidthF); - } - int maxWidth = (int) (maxWidthF + 0.5); - if (maxWidth < currentMax) { - maxWidth = currentMax; - } - return maxWidth; - } - - private void saveSelectionPosition(float left, float top, float right, float bottom) { - mPrevBox.left = (int) left; - mPrevBox.right = (int) right; - mPrevBox.top = (int) top; - mPrevBox.bottom = (int) bottom; - } - - private void setupTextRect(Rect r) { - if (r.bottom <= r.top || r.right <= r.left) { - r.bottom = r.top; - r.right = r.left; - return; - } - - if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) { - r.top += EVENT_TEXT_TOP_MARGIN; - r.bottom -= EVENT_TEXT_BOTTOM_MARGIN; - } - if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) { - r.left += EVENT_TEXT_LEFT_MARGIN; - r.right -= EVENT_TEXT_RIGHT_MARGIN; - } - } - - private void setupAllDayTextRect(Rect r) { - if (r.bottom <= r.top || r.right <= r.left) { - r.bottom = r.top; - r.right = r.left; - return; - } - - if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) { - r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN; - r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN; - } - if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) { - r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN; - r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN; - } - } - - /** - * Return the layout for a numbered event. Create it if not already existing - */ - private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint, - Rect r) { - if (i < 0 || i >= layouts.length) { - return null; - } - - StaticLayout layout = layouts[i]; - // Check if we have already initialized the StaticLayout and that - // the width hasn't changed (due to vertical resizing which causes - // re-layout of events at min height) - if (layout == null || r.width() != layout.getWidth()) { - SpannableStringBuilder bob = new SpannableStringBuilder(); - if (event.title != null) { - // MAX - 1 since we add a space - bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN - 1)); - bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0); - bob.append(' '); - } - if (event.location != null) { - bob.append(drawTextSanitizer(event.location.toString(), - MAX_EVENT_TEXT_LEN - bob.length())); - } - - switch (event.selfAttendeeStatus) { - case Attendees.ATTENDEE_STATUS_INVITED: - paint.setColor(event.color); - break; - case Attendees.ATTENDEE_STATUS_DECLINED: - paint.setColor(mEventTextColor); - paint.setAlpha(Utils.DECLINED_EVENT_TEXT_ALPHA); - break; - case Attendees.ATTENDEE_STATUS_NONE: // Your own events - case Attendees.ATTENDEE_STATUS_ACCEPTED: - case Attendees.ATTENDEE_STATUS_TENTATIVE: - default: - paint.setColor(mEventTextColor); - break; - } - - // Leave a one pixel boundary on the left and right of the rectangle for the event - layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(), - Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()); - - layouts[i] = layout; - } - layout.getPaint().setAlpha(mEventsAlpha); - return layout; - } - - private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) { - - p.setTextSize(NORMAL_FONT_SIZE); - p.setTextAlign(Paint.Align.LEFT); - Paint eventTextPaint = mEventTextPaint; - - final float startY = DAY_HEADER_HEIGHT; - final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN; - float x = 0; - int linesIndex = 0; - - // Draw the inner vertical grid lines - p.setColor(mCalendarGridLineInnerVerticalColor); - x = mHoursWidth; - p.setStrokeWidth(GRID_LINE_INNER_WIDTH); - // Line bounding the top of the all day area - mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; - mLines[linesIndex++] = startY; - mLines[linesIndex++] = computeDayLeftPosition(mNumDays); - mLines[linesIndex++] = startY; - - for (int day = 0; day <= mNumDays; day++) { - x = computeDayLeftPosition(day); - mLines[linesIndex++] = x; - mLines[linesIndex++] = startY; - mLines[linesIndex++] = x; - mLines[linesIndex++] = stopY; - } - p.setAntiAlias(false); - canvas.drawLines(mLines, 0, linesIndex, p); - p.setStyle(Style.FILL); - - int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; - int lastDay = firstDay + numDays - 1; - final ArrayList<Event> events = mAllDayEvents; - int numEvents = events.size(); - // Whether or not we should draw the more events text - boolean hasMoreEvents = false; - // size of the allDay area - float drawHeight = mAlldayHeight; - // max number of events being drawn in one day of the allday area - float numRectangles = mMaxAlldayEvents; - // Where to cut off drawn allday events - int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN; - // The number of events that weren't drawn in each day - mSkippedAlldayEvents = new int[numDays]; - if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents && - mAnimateDayHeight == 0) { - // We draw one fewer event than will fit so that more events text - // can be drawn - numRectangles = mMaxUnexpandedAlldayEventCount - 1; - // We also clip the events above the more events text - allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; - hasMoreEvents = true; - } else if (mAnimateDayHeight != 0) { - // clip at the end of the animating space - allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN; - } - - int alpha = eventTextPaint.getAlpha(); - eventTextPaint.setAlpha(mEventsAlpha); - for (int i = 0; i < numEvents; i++) { - Event event = events.get(i); - int startDay = event.startDay; - int endDay = event.endDay; - if (startDay > lastDay || endDay < firstDay) { - continue; - } - if (startDay < firstDay) { - startDay = firstDay; - } - if (endDay > lastDay) { - endDay = lastDay; - } - int startIndex = startDay - firstDay; - int endIndex = endDay - firstDay; - float height = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight : - drawHeight / numRectangles; - - // Prevent a single event from getting too big - if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { - height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; - } - - // Leave a one-pixel space between the vertical day lines and the - // event rectangle. - event.left = computeDayLeftPosition(startIndex); - event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP; - event.top = y + height * event.getColumn(); - event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN; - if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { - // check if we should skip this event. We skip if it starts - // after the clip bound or ends after the skip bound and we're - // not animating. - if (event.top >= allDayEventClip) { - incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); - continue; - } else if (event.bottom > allDayEventClip) { - if (hasMoreEvents) { - incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); - continue; - } - event.bottom = allDayEventClip; - } - } - Rect r = drawEventRect(event, canvas, p, eventTextPaint, (int) event.top, - (int) event.bottom); - setupAllDayTextRect(r); - StaticLayout layout = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r); - drawEventText(layout, r, canvas, r.top, r.bottom, true); - - // Check if this all-day event intersects the selected day - if (mSelectionAllday && mComputeSelectedEvents) { - if (startDay <= mSelectionDay && endDay >= mSelectionDay) { - mSelectedEvents.add(event); - } - } - } - eventTextPaint.setAlpha(alpha); - - if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) { - // If the more allday text should be visible, draw it. - alpha = p.getAlpha(); - p.setAlpha(mEventsAlpha); - p.setColor(mMoreAlldayEventsTextAlpha << 24 & mMoreEventsTextColor); - for (int i = 0; i < mSkippedAlldayEvents.length; i++) { - if (mSkippedAlldayEvents[i] > 0) { - drawMoreAlldayEvents(canvas, mSkippedAlldayEvents[i], i, p); - } - } - p.setAlpha(alpha); - } - - if (mSelectionAllday) { - // Compute the neighbors for the list of all-day events that - // intersect the selected day. - computeAllDayNeighbors(); - - // Set the selection position to zero so that when we move down - // to the normal event area, we will highlight the topmost event. - saveSelectionPosition(0f, 0f, 0f, 0f); - } - } - - // Helper method for counting the number of allday events skipped on each day - private void incrementSkipCount(int[] counts, int startIndex, int endIndex) { - if (counts == null || startIndex < 0 || endIndex > counts.length) { - return; - } - for (int i = startIndex; i <= endIndex; i++) { - counts[i]++; - } - } - - // Draws the "box +n" text for hidden allday events - protected void drawMoreAlldayEvents(Canvas canvas, int remainingEvents, int day, Paint p) { - int x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN; - int y = (int) (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - .5f - * EVENT_SQUARE_WIDTH + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN); - Rect r = mRect; - r.top = y; - r.left = x; - r.bottom = y + EVENT_SQUARE_WIDTH; - r.right = x + EVENT_SQUARE_WIDTH; - p.setColor(mMoreEventsTextColor); - p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); - p.setStyle(Style.STROKE); - p.setAntiAlias(false); - canvas.drawRect(r, p); - p.setAntiAlias(true); - p.setStyle(Style.FILL); - p.setTextSize(EVENT_TEXT_FONT_SIZE); - String text = mResources.getQuantityString(R.plurals.month_more_events, remainingEvents); - y += EVENT_SQUARE_WIDTH; - x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING; - canvas.drawText(String.format(text, remainingEvents), x, y, p); - } - - private void computeAllDayNeighbors() { - int len = mSelectedEvents.size(); - if (len == 0 || mSelectedEvent != null) { - return; - } - - // First, clear all the links - for (int ii = 0; ii < len; ii++) { - Event ev = mSelectedEvents.get(ii); - ev.nextUp = null; - ev.nextDown = null; - ev.nextLeft = null; - ev.nextRight = null; - } - - // For each event in the selected event list "mSelectedEvents", find - // its neighbors in the up and down directions. This could be done - // more efficiently by sorting on the Event.getColumn() field, but - // the list is expected to be very small. - - // Find the event in the same row as the previously selected all-day - // event, if any. - int startPosition = -1; - if (mPrevSelectedEvent != null && mPrevSelectedEvent.drawAsAllday()) { - startPosition = mPrevSelectedEvent.getColumn(); - } - int maxPosition = -1; - Event startEvent = null; - Event maxPositionEvent = null; - for (int ii = 0; ii < len; ii++) { - Event ev = mSelectedEvents.get(ii); - int position = ev.getColumn(); - if (position == startPosition) { - startEvent = ev; - } else if (position > maxPosition) { - maxPositionEvent = ev; - maxPosition = position; - } - for (int jj = 0; jj < len; jj++) { - if (jj == ii) { - continue; - } - Event neighbor = mSelectedEvents.get(jj); - int neighborPosition = neighbor.getColumn(); - if (neighborPosition == position - 1) { - ev.nextUp = neighbor; - } else if (neighborPosition == position + 1) { - ev.nextDown = neighbor; - } - } - } - if (startEvent != null) { - setSelectedEvent(startEvent); - } else { - setSelectedEvent(maxPositionEvent); - } - } - - private void drawEvents(int date, int dayIndex, int top, Canvas canvas, Paint p) { - Paint eventTextPaint = mEventTextPaint; - int left = computeDayLeftPosition(dayIndex) + 1; - int cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1; - int cellHeight = mCellHeight; - - // Use the selected hour as the selection region - Rect selectionArea = mSelectionRect; - selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP); - selectionArea.bottom = selectionArea.top + cellHeight; - selectionArea.left = left; - selectionArea.right = selectionArea.left + cellWidth; - - final ArrayList<Event> events = mEvents; - int numEvents = events.size(); - EventGeometry geometry = mEventGeometry; - - final int viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight; - - int alpha = eventTextPaint.getAlpha(); - eventTextPaint.setAlpha(mEventsAlpha); - for (int i = 0; i < numEvents; i++) { - Event event = events.get(i); - if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { - continue; - } - - // Don't draw it if it is not visible - if (event.bottom < mViewStartY || event.top > viewEndY) { - continue; - } - - if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents - && geometry.eventIntersectsSelection(event, selectionArea)) { - mSelectedEvents.add(event); - } - - Rect r = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY); - setupTextRect(r); - - // Don't draw text if it is not visible - if (r.top > viewEndY || r.bottom < mViewStartY) { - continue; - } - StaticLayout layout = getEventLayout(mLayouts, i, event, eventTextPaint, r); - // TODO: not sure why we are 4 pixels off - drawEventText(layout, r, canvas, mViewStartY + 4, mViewStartY + mViewHeight - - DAY_HEADER_HEIGHT - mAlldayHeight, false); - } - eventTextPaint.setAlpha(alpha); - } - - private Rect drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint, - int visibleTop, int visibleBot) { - // Draw the Event Rect - Rect r = mRect; - r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN, visibleTop); - r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN, visibleBot); - r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; - r.right = (int) event.right; - - int color = event.color; - switch (event.selfAttendeeStatus) { - case Attendees.ATTENDEE_STATUS_INVITED: - if (event != mClickedEvent) { - p.setStyle(Style.STROKE); - } - break; - case Attendees.ATTENDEE_STATUS_DECLINED: - if (event != mClickedEvent) { - color = Utils.getDeclinedColorFromColor(color); - } - case Attendees.ATTENDEE_STATUS_NONE: // Your own events - case Attendees.ATTENDEE_STATUS_ACCEPTED: - case Attendees.ATTENDEE_STATUS_TENTATIVE: - default: - p.setStyle(Style.FILL_AND_STROKE); - break; - } - - p.setAntiAlias(false); - - int floorHalfStroke = (int) Math.floor(EVENT_RECT_STROKE_WIDTH / 2.0f); - int ceilHalfStroke = (int) Math.ceil(EVENT_RECT_STROKE_WIDTH / 2.0f); - r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop); - r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke, - visibleBot); - r.left += floorHalfStroke; - r.right -= ceilHalfStroke; - p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); - p.setColor(color); - int alpha = p.getAlpha(); - p.setAlpha(mEventsAlpha); - canvas.drawRect(r, p); - p.setAlpha(alpha); - p.setStyle(Style.FILL); - - // Setup rect for drawEventText which follows - r.top = (int) event.top + EVENT_RECT_TOP_MARGIN; - r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN; - r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; - r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN; - return r; - } - - private final Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],"); - - // Sanitize a string before passing it to drawText or else we get little - // squares. For newlines and tabs before a comma, delete the character. - // Otherwise, just replace them with a space. - private String drawTextSanitizer(String string, int maxEventTextLen) { - Matcher m = drawTextSanitizerFilter.matcher(string); - string = m.replaceAll(","); - - int len = string.length(); - if (maxEventTextLen <= 0) { - string = ""; - len = 0; - } else if (len > maxEventTextLen) { - string = string.substring(0, maxEventTextLen); - len = maxEventTextLen; - } - - return string.replace('\n', ' '); - } - - private void drawEventText(StaticLayout eventLayout, Rect rect, Canvas canvas, int top, - int bottom, boolean center) { - // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging - - int width = rect.right - rect.left; - int height = rect.bottom - rect.top; - - // If the rectangle is too small for text, then return - if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) { - return; - } - - int totalLineHeight = 0; - int lineCount = eventLayout.getLineCount(); - for (int i = 0; i < lineCount; i++) { - int lineBottom = eventLayout.getLineBottom(i); - if (lineBottom <= height) { - totalLineHeight = lineBottom; - } else { - break; - } - } - - // + 2 is small workaround when the font is slightly bigger then the rect. This will - // still allow the text to be shown without overflowing into the other all day rects. - if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight + 2 < top) { - return; - } - - // Use a StaticLayout to format the string. - canvas.save(); - // canvas.translate(rect.left, rect.top + (rect.bottom - rect.top / 2)); - int padding = center? (rect.bottom - rect.top - totalLineHeight) / 2 : 0; - canvas.translate(rect.left, rect.top + padding); - rect.left = 0; - rect.right = width; - rect.top = 0; - rect.bottom = totalLineHeight; - - // There's a bug somewhere. If this rect is outside of a previous - // cliprect, this becomes a no-op. What happens is that the text draw - // past the event rect. The current fix is to not draw the staticLayout - // at all if it is completely out of bound. - canvas.clipRect(rect); - eventLayout.draw(canvas); - canvas.restore(); - } - - // The following routines are called from the parent activity when certain - // touch events occur. - private void doDown(MotionEvent ev) { - mTouchMode = TOUCH_MODE_DOWN; - mViewStartX = 0; - mOnFlingCalled = false; - mHandler.removeCallbacks(mContinueScroll); - int x = (int) ev.getX(); - int y = (int) ev.getY(); - - // Save selection information: we use setSelectionFromPosition to find the selected event - // in order to show the "clicked" color. But since it is also setting the selected info - // for new events, we need to restore the old info after calling the function. - Event oldSelectedEvent = mSelectedEvent; - int oldSelectionDay = mSelectionDay; - int oldSelectionHour = mSelectionHour; - if (setSelectionFromPosition(x, y, false)) { - // If a time was selected (a blue selection box is visible) and the click location - // is in the selected time, do not show a click on an event to prevent a situation - // of both a selection and an event are clicked when they overlap. - boolean pressedSelected = (mSelectionMode != SELECTION_HIDDEN) - && oldSelectionDay == mSelectionDay && oldSelectionHour == mSelectionHour; - if (!pressedSelected && mSelectedEvent != null) { - mSavedClickedEvent = mSelectedEvent; - mDownTouchTime = System.currentTimeMillis(); - postDelayed (mSetClick,mOnDownDelay); - } else { - eventClickCleanup(); - } - } - mSelectedEvent = oldSelectedEvent; - mSelectionDay = oldSelectionDay; - mSelectionHour = oldSelectionHour; - invalidate(); - } - - // Kicks off all the animations when the expand allday area is tapped - private void doExpandAllDayClick() { - mShowAllAllDayEvents = !mShowAllAllDayEvents; - - ObjectAnimator.setFrameDelay(0); - - // Determine the starting height - if (mAnimateDayHeight == 0) { - mAnimateDayHeight = mShowAllAllDayEvents ? - mAlldayHeight - (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT : mAlldayHeight; - } - // Cancel current animations - mCancellingAnimations = true; - if (mAlldayAnimator != null) { - mAlldayAnimator.cancel(); - } - if (mAlldayEventAnimator != null) { - mAlldayEventAnimator.cancel(); - } - if (mMoreAlldayEventsAnimator != null) { - mMoreAlldayEventsAnimator.cancel(); - } - mCancellingAnimations = false; - // get new animators - mAlldayAnimator = getAllDayAnimator(); - mAlldayEventAnimator = getAllDayEventAnimator(); - mMoreAlldayEventsAnimator = ObjectAnimator.ofInt(this, - "moreAllDayEventsTextAlpha", - mShowAllAllDayEvents ? MORE_EVENTS_MAX_ALPHA : 0, - mShowAllAllDayEvents ? 0 : MORE_EVENTS_MAX_ALPHA); - - // Set up delays and start the animators - mAlldayAnimator.setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); - mAlldayAnimator.start(); - mMoreAlldayEventsAnimator.setStartDelay(mShowAllAllDayEvents ? 0 : ANIMATION_DURATION); - mMoreAlldayEventsAnimator.setDuration(ANIMATION_SECONDARY_DURATION); - mMoreAlldayEventsAnimator.start(); - if (mAlldayEventAnimator != null) { - // This is the only animator that can return null, so check it - mAlldayEventAnimator - .setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); - mAlldayEventAnimator.start(); - } - } - - /** - * Figures out the initial heights for allDay events and space when - * a view is being set up. - */ - public void initAllDayHeights() { - if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) { - return; - } - if (mShowAllAllDayEvents) { - int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; - maxADHeight = Math.min(maxADHeight, - (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); - mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents; - } else { - mAnimateDayEventHeight = (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; - } - } - - // Sets up an animator for changing the height of allday events - private ObjectAnimator getAllDayEventAnimator() { - // First calculate the absolute max height - int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; - // Now expand to fit but not beyond the absolute max - maxADHeight = - Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); - // calculate the height of individual events in order to fit - int fitHeight = maxADHeight / mMaxAlldayEvents; - int currentHeight = mAnimateDayEventHeight; - int desiredHeight = - mShowAllAllDayEvents ? fitHeight : (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; - // if there's nothing to animate just return - if (currentHeight == desiredHeight) { - return null; - } - - // Set up the animator with the calculated values - ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayEventHeight", - currentHeight, desiredHeight); - animator.setDuration(ANIMATION_DURATION); - return animator; - } - - // Sets up an animator for changing the height of the allday area - private ObjectAnimator getAllDayAnimator() { - // Calculate the absolute max height - int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; - // Find the desired height but don't exceed abs max - maxADHeight = - Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); - // calculate the current and desired heights - int currentHeight = mAnimateDayHeight != 0 ? mAnimateDayHeight : mAlldayHeight; - int desiredHeight = mShowAllAllDayEvents ? maxADHeight : - (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1); - - // Set up the animator with the calculated values - ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayHeight", - currentHeight, desiredHeight); - animator.setDuration(ANIMATION_DURATION); - - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (!mCancellingAnimations) { - // when finished, set this to 0 to signify not animating - mAnimateDayHeight = 0; - mUseExpandIcon = !mShowAllAllDayEvents; - } - mRemeasure = true; - invalidate(); - } - }); - return animator; - } - - // setter for the 'box +n' alpha text used by the animator - public void setMoreAllDayEventsTextAlpha(int alpha) { - mMoreAlldayEventsTextAlpha = alpha; - invalidate(); - } - - // setter for the height of the allday area used by the animator - public void setAnimateDayHeight(int height) { - mAnimateDayHeight = height; - mRemeasure = true; - invalidate(); - } - - // setter for the height of allday events used by the animator - public void setAnimateDayEventHeight(int height) { - mAnimateDayEventHeight = height; - mRemeasure = true; - invalidate(); - } - - private void doSingleTapUp(MotionEvent ev) { - if (!mHandleActionUp || mScrolling) { - return; - } - - int x = (int) ev.getX(); - int y = (int) ev.getY(); - int selectedDay = mSelectionDay; - int selectedHour = mSelectionHour; - - if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { - // check if the tap was in the allday expansion area - int bottom = mFirstCell; - if((x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight) - || (!mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom && - y >= bottom - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)) { - doExpandAllDayClick(); - return; - } - } - - boolean validPosition = setSelectionFromPosition(x, y, false); - if (!validPosition) { - if (y < DAY_HEADER_HEIGHT) { - Time selectedTime = new Time(mBaseDate); - selectedTime.setJulianDay(mSelectionDay); - selectedTime.hour = mSelectionHour; - selectedTime.normalize(true /* ignore isDst */); - mController.sendEvent(this, EventType.GO_TO, null, null, selectedTime, -1, - ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null); - } - return; - } - - boolean hasSelection = mSelectionMode != SELECTION_HIDDEN; - boolean pressedSelected = (hasSelection || mTouchExplorationEnabled) - && selectedDay == mSelectionDay && selectedHour == mSelectionHour; - - if (mSelectedEvent != null) { - // If the tap is on an event, launch the "View event" view - if (mIsAccessibilityEnabled) { - mAccessibilityMgr.interrupt(); - } - - mSelectionMode = SELECTION_HIDDEN; - - int yLocation = - (int)((mSelectedEvent.top + mSelectedEvent.bottom)/2); - // Y location is affected by the position of the event in the scrolling - // view (mViewStartY) and the presence of all day events (mFirstCell) - if (!mSelectedEvent.allDay) { - yLocation += (mFirstCell - mViewStartY); - } - mClickedYLocation = yLocation; - long clearDelay = (CLICK_DISPLAY_DURATION + mOnDownDelay) - - (System.currentTimeMillis() - mDownTouchTime); - if (clearDelay > 0) { - this.postDelayed(mClearClick, clearDelay); - } else { - this.post(mClearClick); - } - } - invalidate(); - } - - private void doLongPress(MotionEvent ev) { - eventClickCleanup(); - if (mScrolling) { - return; - } - - // Scale gesture in progress - if (mStartingSpanY != 0) { - return; - } - - int x = (int) ev.getX(); - int y = (int) ev.getY(); - - boolean validPosition = setSelectionFromPosition(x, y, false); - if (!validPosition) { - // return if the touch wasn't on an area of concern - return; - } - - invalidate(); - performLongClick(); - } - - private void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) { - cancelAnimation(); - if (mStartingScroll) { - mInitialScrollX = 0; - mInitialScrollY = 0; - mStartingScroll = false; - } - - mInitialScrollX += deltaX; - mInitialScrollY += deltaY; - int distanceX = (int) mInitialScrollX; - int distanceY = (int) mInitialScrollY; - - final float focusY = getAverageY(e2); - if (mRecalCenterHour) { - // Calculate the hour that correspond to the average of the Y touch points - mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) - / (mCellHeight + DAY_GAP); - mRecalCenterHour = false; - } - - // If we haven't figured out the predominant scroll direction yet, - // then do it now. - if (mTouchMode == TOUCH_MODE_DOWN) { - int absDistanceX = Math.abs(distanceX); - int absDistanceY = Math.abs(distanceY); - mScrollStartY = mViewStartY; - mPreviousDirection = 0; - - if (absDistanceX > absDistanceY) { - int slopFactor = mScaleGestureDetector.isInProgress() ? 20 : 2; - if (absDistanceX > mScaledPagingTouchSlop * slopFactor) { - mTouchMode = TOUCH_MODE_HSCROLL; - mViewStartX = distanceX; - initNextView(-mViewStartX); - } - } else { - mTouchMode = TOUCH_MODE_VSCROLL; - } - } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { - // We are already scrolling horizontally, so check if we - // changed the direction of scrolling so that the other week - // is now visible. - mViewStartX = distanceX; - if (distanceX != 0) { - int direction = (distanceX > 0) ? 1 : -1; - if (direction != mPreviousDirection) { - // The user has switched the direction of scrolling - // so re-init the next view - initNextView(-mViewStartX); - mPreviousDirection = direction; - } - } - } - - if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) { - // Calculate the top of the visible region in the calendar grid. - // Increasing/decrease this will scroll the calendar grid up/down. - mViewStartY = (int) ((mGestureCenterHour * (mCellHeight + DAY_GAP)) - - focusY + DAY_HEADER_HEIGHT + mAlldayHeight); - - // If dragging while already at the end, do a glow - final int pulledToY = (int) (mScrollStartY + deltaY); - if (pulledToY < 0) { - mEdgeEffectTop.onPull(deltaY / mViewHeight); - if (!mEdgeEffectBottom.isFinished()) { - mEdgeEffectBottom.onRelease(); - } - } else if (pulledToY > mMaxViewStartY) { - mEdgeEffectBottom.onPull(deltaY / mViewHeight); - if (!mEdgeEffectTop.isFinished()) { - mEdgeEffectTop.onRelease(); - } - } - - if (mViewStartY < 0) { - mViewStartY = 0; - mRecalCenterHour = true; - } else if (mViewStartY > mMaxViewStartY) { - mViewStartY = mMaxViewStartY; - mRecalCenterHour = true; - } - if (mRecalCenterHour) { - // Calculate the hour that correspond to the average of the Y touch points - mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) - / (mCellHeight + DAY_GAP); - mRecalCenterHour = false; - } - computeFirstHour(); - } - - mScrolling = true; - - mSelectionMode = SELECTION_HIDDEN; - invalidate(); - } - - private float getAverageY(MotionEvent me) { - int count = me.getPointerCount(); - float focusY = 0; - for (int i = 0; i < count; i++) { - focusY += me.getY(i); - } - focusY /= count; - return focusY; - } - - private void cancelAnimation() { - Animation in = mViewSwitcher.getInAnimation(); - if (in != null) { - // cancel() doesn't terminate cleanly. - in.scaleCurrentDuration(0); - } - Animation out = mViewSwitcher.getOutAnimation(); - if (out != null) { - // cancel() doesn't terminate cleanly. - out.scaleCurrentDuration(0); - } - } - - private void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - cancelAnimation(); - - mSelectionMode = SELECTION_HIDDEN; - eventClickCleanup(); - - mOnFlingCalled = true; - - if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { - // Horizontal fling. - // initNextView(deltaX); - mTouchMode = TOUCH_MODE_INITIAL_STATE; - if (DEBUG) Log.d(TAG, "doFling: velocityX " + velocityX); - int deltaX = (int) e2.getX() - (int) e1.getX(); - switchViews(deltaX < 0, mViewStartX, mViewWidth, velocityX); - mViewStartX = 0; - return; - } - - if ((mTouchMode & TOUCH_MODE_VSCROLL) == 0) { - if (DEBUG) Log.d(TAG, "doFling: no fling"); - return; - } - - // Vertical fling. - mTouchMode = TOUCH_MODE_INITIAL_STATE; - mViewStartX = 0; - - if (DEBUG) { - Log.d(TAG, "doFling: mViewStartY" + mViewStartY + " velocityY " + velocityY); - } - - // Continue scrolling vertically - mScrolling = true; - mScroller.fling(0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */, - (int) -velocityY, 0 /* minX */, 0 /* maxX */, 0 /* minY */, - mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE); - - // When flinging down, show a glow when it hits the end only if it - // wasn't started at the top - if (velocityY > 0 && mViewStartY != 0) { - mCallEdgeEffectOnAbsorb = true; - } - // When flinging up, show a glow when it hits the end only if it wasn't - // started at the bottom - else if (velocityY < 0 && mViewStartY != mMaxViewStartY) { - mCallEdgeEffectOnAbsorb = true; - } - mHandler.post(mContinueScroll); - } - - private boolean initNextView(int deltaX) { - // Change the view to the previous day or week - DayView view = (DayView) mViewSwitcher.getNextView(); - Time date = view.mBaseDate; - date.set(mBaseDate); - boolean switchForward; - if (deltaX > 0) { - date.monthDay -= mNumDays; - view.setSelectedDay(mSelectionDay - mNumDays); - switchForward = false; - } else { - date.monthDay += mNumDays; - view.setSelectedDay(mSelectionDay + mNumDays); - switchForward = true; - } - date.normalize(true /* ignore isDst */); - initView(view); - view.layout(getLeft(), getTop(), getRight(), getBottom()); - view.reloadEvents(); - return switchForward; - } - - // ScaleGestureDetector.OnScaleGestureListener - public boolean onScaleBegin(ScaleGestureDetector detector) { - mHandleActionUp = false; - float gestureCenterInPixels = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; - mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP); - - mStartingSpanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); - mCellHeightBeforeScaleGesture = mCellHeight; - - if (DEBUG_SCALING) { - float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); - Log.d(TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour - + "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY - + "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); - } - - return true; - } - - // ScaleGestureDetector.OnScaleGestureListener - public boolean onScale(ScaleGestureDetector detector) { - float spanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); - - mCellHeight = (int) (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY); - - if (mCellHeight < mMinCellHeight) { - // If mStartingSpanY is too small, even a small increase in the - // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT - mStartingSpanY = spanY; - mCellHeight = mMinCellHeight; - mCellHeightBeforeScaleGesture = mMinCellHeight; - } else if (mCellHeight > MAX_CELL_HEIGHT) { - mStartingSpanY = spanY; - mCellHeight = MAX_CELL_HEIGHT; - mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT; - } - - int gestureCenterInPixels = (int) detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; - mViewStartY = (int) (mGestureCenterHour * (mCellHeight + DAY_GAP)) - gestureCenterInPixels; - mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; - - if (DEBUG_SCALING) { - float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); - Log.d(TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " - + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" - + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); - } - - if (mViewStartY < 0) { - mViewStartY = 0; - mGestureCenterHour = (mViewStartY + gestureCenterInPixels) - / (float) (mCellHeight + DAY_GAP); - } else if (mViewStartY > mMaxViewStartY) { - mViewStartY = mMaxViewStartY; - mGestureCenterHour = (mViewStartY + gestureCenterInPixels) - / (float) (mCellHeight + DAY_GAP); - } - computeFirstHour(); - - mRemeasure = true; - invalidate(); - return true; - } - - // ScaleGestureDetector.OnScaleGestureListener - public void onScaleEnd(ScaleGestureDetector detector) { - mScrollStartY = mViewStartY; - mInitialScrollY = 0; - mInitialScrollX = 0; - mStartingSpanY = 0; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - int action = ev.getAction(); - if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount()); - - if ((ev.getActionMasked() == MotionEvent.ACTION_DOWN) || - (ev.getActionMasked() == MotionEvent.ACTION_UP) || - (ev.getActionMasked() == MotionEvent.ACTION_POINTER_UP) || - (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN)) { - mRecalCenterHour = true; - } - - if ((mTouchMode & TOUCH_MODE_HSCROLL) == 0) { - mScaleGestureDetector.onTouchEvent(ev); - } - - switch (action) { - case MotionEvent.ACTION_DOWN: - mStartingScroll = true; - if (DEBUG) { - Log.e(TAG, "ACTION_DOWN ev.getDownTime = " + ev.getDownTime() + " Cnt=" - + ev.getPointerCount()); - } - - int bottom = mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; - if (ev.getY() < bottom) { - mTouchStartedInAlldayArea = true; - } else { - mTouchStartedInAlldayArea = false; - } - mHandleActionUp = true; - mGestureDetector.onTouchEvent(ev); - return true; - - case MotionEvent.ACTION_MOVE: - if (DEBUG) Log.e(TAG, "ACTION_MOVE Cnt=" + ev.getPointerCount() + DayView.this); - mGestureDetector.onTouchEvent(ev); - return true; - - case MotionEvent.ACTION_UP: - if (DEBUG) Log.e(TAG, "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp); - mEdgeEffectTop.onRelease(); - mEdgeEffectBottom.onRelease(); - mStartingScroll = false; - mGestureDetector.onTouchEvent(ev); - if (!mHandleActionUp) { - mHandleActionUp = true; - mViewStartX = 0; - invalidate(); - return true; - } - - if (mOnFlingCalled) { - return true; - } - - // If we were scrolling, then reset the selected hour so that it - // is visible. - if (mScrolling) { - mScrolling = false; - resetSelectedHour(); - invalidate(); - } - - if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { - mTouchMode = TOUCH_MODE_INITIAL_STATE; - if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) { - // The user has gone beyond the threshold so switch views - if (DEBUG) Log.d(TAG, "- horizontal scroll: switch views"); - switchViews(mViewStartX > 0, mViewStartX, mViewWidth, 0); - mViewStartX = 0; - return true; - } else { - // Not beyond the threshold so invalidate which will cause - // the view to snap back. Also call recalc() to ensure - // that we have the correct starting date and title. - if (DEBUG) Log.d(TAG, "- horizontal scroll: snap back"); - recalc(); - invalidate(); - mViewStartX = 0; - } - } - - return true; - - // This case isn't expected to happen. - case MotionEvent.ACTION_CANCEL: - if (DEBUG) Log.e(TAG, "ACTION_CANCEL"); - mGestureDetector.onTouchEvent(ev); - mScrolling = false; - resetSelectedHour(); - return true; - - default: - if (DEBUG) Log.e(TAG, "Not MotionEvent " + ev.toString()); - if (mGestureDetector.onTouchEvent(ev)) { - return true; - } - return super.onTouchEvent(ev); - } - } - - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - MenuItem item; - - // If the trackball is held down, then the context menu pops up and - // we never get onKeyUp() for the long-press. So check for it here - // and change the selection to the long-press state. - if (mSelectionMode != SELECTION_LONGPRESS) { - invalidate(); - } - - final long startMillis = getSelectedTimeInMillis(); - int flags = DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_CAP_NOON_MIDNIGHT - | DateUtils.FORMAT_SHOW_WEEKDAY; - final String title = Utils.formatDateRange(mContext, startMillis, startMillis, flags); - menu.setHeaderTitle(title); - - mPopup.dismiss(); - } - - /** - * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position. - * If the touch position is not within the displayed grid, then this - * method returns false. - * - * @param x the x position of the touch - * @param y the y position of the touch - * @param keepOldSelection - do not change the selection info (used for invoking accessibility - * messages) - * @return true if the touch position is valid - */ - private boolean setSelectionFromPosition(int x, final int y, boolean keepOldSelection) { - - Event savedEvent = null; - int savedDay = 0; - int savedHour = 0; - boolean savedAllDay = false; - if (keepOldSelection) { - // Store selection info and restore it at the end. This way, we can invoke the - // right accessibility message without affecting the selection. - savedEvent = mSelectedEvent; - savedDay = mSelectionDay; - savedHour = mSelectionHour; - savedAllDay = mSelectionAllday; - } - if (x < mHoursWidth) { - x = mHoursWidth; - } - - int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP); - if (day >= mNumDays) { - day = mNumDays - 1; - } - day += mFirstJulianDay; - setSelectedDay(day); - - if (y < DAY_HEADER_HEIGHT) { - sendAccessibilityEventAsNeeded(false); - return false; - } - - setSelectedHour(mFirstHour); /* First fully visible hour */ - - if (y < mFirstCell) { - mSelectionAllday = true; - } else { - // y is now offset from top of the scrollable region - int adjustedY = y - mFirstCell; - - if (adjustedY < mFirstHourOffset) { - setSelectedHour(mSelectionHour - 1); /* In the partially visible hour */ - } else { - setSelectedHour(mSelectionHour + - (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP)); - } - - mSelectionAllday = false; - } - - findSelectedEvent(x, y); - - sendAccessibilityEventAsNeeded(true); - - // Restore old values - if (keepOldSelection) { - mSelectedEvent = savedEvent; - mSelectionDay = savedDay; - mSelectionHour = savedHour; - mSelectionAllday = savedAllDay; - } - return true; - } - - private void findSelectedEvent(int x, int y) { - int date = mSelectionDay; - int cellWidth = mCellWidth; - ArrayList<Event> events = mEvents; - int numEvents = events.size(); - int left = computeDayLeftPosition(mSelectionDay - mFirstJulianDay); - int top = 0; - setSelectedEvent(null); - - mSelectedEvents.clear(); - if (mSelectionAllday) { - float yDistance; - float minYdistance = 10000.0f; // any large number - Event closestEvent = null; - float drawHeight = mAlldayHeight; - int yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; - int maxUnexpandedColumn = mMaxUnexpandedAlldayEventCount; - if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { - // Leave a gap for the 'box +n' text - maxUnexpandedColumn--; - } - events = mAllDayEvents; - numEvents = events.size(); - for (int i = 0; i < numEvents; i++) { - Event event = events.get(i); - if (!event.drawAsAllday() || - (!mShowAllAllDayEvents && event.getColumn() >= maxUnexpandedColumn)) { - // Don't check non-allday events or events that aren't shown - continue; - } - - if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) { - float numRectangles = mShowAllAllDayEvents ? mMaxAlldayEvents - : mMaxUnexpandedAlldayEventCount; - float height = drawHeight / numRectangles; - if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { - height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; - } - float eventTop = yOffset + height * event.getColumn(); - float eventBottom = eventTop + height; - if (eventTop < y && eventBottom > y) { - // If the touch is inside the event rectangle, then - // add the event. - mSelectedEvents.add(event); - closestEvent = event; - break; - } else { - // Find the closest event - if (eventTop >= y) { - yDistance = eventTop - y; - } else { - yDistance = y - eventBottom; - } - if (yDistance < minYdistance) { - minYdistance = yDistance; - closestEvent = event; - } - } - } - } - setSelectedEvent(closestEvent); - return; - } - - // Adjust y for the scrollable bitmap - y += mViewStartY - mFirstCell; - - // Use a region around (x,y) for the selection region - Rect region = mRect; - region.left = x - 10; - region.right = x + 10; - region.top = y - 10; - region.bottom = y + 10; - - EventGeometry geometry = mEventGeometry; - - for (int i = 0; i < numEvents; i++) { - Event event = events.get(i); - // Compute the event rectangle. - if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { - continue; - } - - // If the event intersects the selection region, then add it to - // mSelectedEvents. - if (geometry.eventIntersectsSelection(event, region)) { - mSelectedEvents.add(event); - } - } - - // If there are any events in the selected region, then assign the - // closest one to mSelectedEvent. - if (mSelectedEvents.size() > 0) { - int len = mSelectedEvents.size(); - Event closestEvent = null; - float minDist = mViewWidth + mViewHeight; // some large distance - for (int index = 0; index < len; index++) { - Event ev = mSelectedEvents.get(index); - float dist = geometry.pointToEvent(x, y, ev); - if (dist < minDist) { - minDist = dist; - closestEvent = ev; - } - } - setSelectedEvent(closestEvent); - - // Keep the selected hour and day consistent with the selected - // event. They could be different if we touched on an empty hour - // slot very close to an event in the previous hour slot. In - // that case we will select the nearby event. - int startDay = mSelectedEvent.startDay; - int endDay = mSelectedEvent.endDay; - if (mSelectionDay < startDay) { - setSelectedDay(startDay); - } else if (mSelectionDay > endDay) { - setSelectedDay(endDay); - } - - int startHour = mSelectedEvent.startTime / 60; - int endHour; - if (mSelectedEvent.startTime < mSelectedEvent.endTime) { - endHour = (mSelectedEvent.endTime - 1) / 60; - } else { - endHour = mSelectedEvent.endTime / 60; - } - - if (mSelectionHour < startHour && mSelectionDay == startDay) { - setSelectedHour(startHour); - } else if (mSelectionHour > endHour && mSelectionDay == endDay) { - setSelectedHour(endHour); - } - } - } - - // Encapsulates the code to continue the scrolling after the - // finger is lifted. Instead of stopping the scroll immediately, - // the scroll continues to "free spin" and gradually slows down. - private class ContinueScroll implements Runnable { - - public void run() { - mScrolling = mScrolling && mScroller.computeScrollOffset(); - if (!mScrolling || mPaused) { - resetSelectedHour(); - invalidate(); - return; - } - - mViewStartY = mScroller.getCurrY(); - - if (mCallEdgeEffectOnAbsorb) { - if (mViewStartY < 0) { - mEdgeEffectTop.onAbsorb((int) mLastVelocity); - mCallEdgeEffectOnAbsorb = false; - } else if (mViewStartY > mMaxViewStartY) { - mEdgeEffectBottom.onAbsorb((int) mLastVelocity); - mCallEdgeEffectOnAbsorb = false; - } - mLastVelocity = mScroller.getCurrVelocity(); - } - - if (mScrollStartY == 0 || mScrollStartY == mMaxViewStartY) { - // Allow overscroll/springback only on a fling, - // not a pull/fling from the end - if (mViewStartY < 0) { - mViewStartY = 0; - } else if (mViewStartY > mMaxViewStartY) { - mViewStartY = mMaxViewStartY; - } - } - - computeFirstHour(); - mHandler.post(this); - invalidate(); - } - } - - /** - * Cleanup the pop-up and timers. - */ - public void cleanup() { - // Protect against null-pointer exceptions - if (mPopup != null) { - mPopup.dismiss(); - } - mPaused = true; - mLastPopupEventID = INVALID_EVENT_ID; - if (mHandler != null) { - mHandler.removeCallbacks(mDismissPopup); - mHandler.removeCallbacks(mUpdateCurrentTime); - } - - Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, - mCellHeight); - // Clear all click animations - eventClickCleanup(); - // Turn off redraw - mRemeasure = false; - // Turn off scrolling to make sure the view is in the correct state if we fling back to it - mScrolling = false; - } - - private void eventClickCleanup() { - this.removeCallbacks(mClearClick); - this.removeCallbacks(mSetClick); - mClickedEvent = null; - mSavedClickedEvent = null; - } - - private void setSelectedEvent(Event e) { - mSelectedEvent = e; - mSelectedEventForAccessibility = e; - } - - private void setSelectedHour(int h) { - mSelectionHour = h; - mSelectionHourForAccessibility = h; - } - private void setSelectedDay(int d) { - mSelectionDay = d; - mSelectionDayForAccessibility = d; - } - - /** - * Restart the update timer - */ - public void restartCurrentTimeUpdates() { - mPaused = false; - if (mHandler != null) { - mHandler.removeCallbacks(mUpdateCurrentTime); - mHandler.post(mUpdateCurrentTime); - } - } - - @Override - protected void onDetachedFromWindow() { - cleanup(); - super.onDetachedFromWindow(); - } - - class DismissPopup implements Runnable { - - public void run() { - // Protect against null-pointer exceptions - if (mPopup != null) { - mPopup.dismiss(); - } - } - } - - class UpdateCurrentTime implements Runnable { - - public void run() { - long currentTime = System.currentTimeMillis(); - mCurrentTime.set(currentTime); - //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.) - if (!DayView.this.mPaused) { - mHandler.postDelayed(mUpdateCurrentTime, UPDATE_CURRENT_TIME_DELAY - - (currentTime % UPDATE_CURRENT_TIME_DELAY)); - } - mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); - invalidate(); - } - } - - class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { - @Override - public boolean onSingleTapUp(MotionEvent ev) { - if (DEBUG) Log.e(TAG, "GestureDetector.onSingleTapUp"); - DayView.this.doSingleTapUp(ev); - return true; - } - - @Override - public void onLongPress(MotionEvent ev) { - if (DEBUG) Log.e(TAG, "GestureDetector.onLongPress"); - DayView.this.doLongPress(ev); - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - if (DEBUG) Log.e(TAG, "GestureDetector.onScroll"); - eventClickCleanup(); - if (mTouchStartedInAlldayArea) { - if (Math.abs(distanceX) < Math.abs(distanceY)) { - // Make sure that click feedback is gone when you scroll from the - // all day area - invalidate(); - return false; - } - // don't scroll vertically if this started in the allday area - distanceY = 0; - } - DayView.this.doScroll(e1, e2, distanceX, distanceY); - return true; - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - if (DEBUG) Log.e(TAG, "GestureDetector.onFling"); - - if (mTouchStartedInAlldayArea) { - if (Math.abs(velocityX) < Math.abs(velocityY)) { - return false; - } - // don't fling vertically if this started in the allday area - velocityY = 0; - } - DayView.this.doFling(e1, e2, velocityX, velocityY); - return true; - } - - @Override - public boolean onDown(MotionEvent ev) { - if (DEBUG) Log.e(TAG, "GestureDetector.onDown"); - DayView.this.doDown(ev); - return true; - } - } - - @Override - public boolean onLongClick(View v) { - return true; - } - - // The rest of this file was borrowed from Launcher2 - PagedView.java - private static final int MINIMUM_SNAP_VELOCITY = 2200; - - private class ScrollInterpolator implements Interpolator { - public ScrollInterpolator() { - } - - public float getInterpolation(float t) { - t -= 1.0f; - t = t * t * t * t * t + 1; - - if ((1 - t) * mAnimationDistance < 1) { - cancelAnimation(); - } - - return t; - } - } - - private long calculateDuration(float delta, float width, float velocity) { - /* - * Here we compute a "distance" that will be used in the computation of - * the overall snap duration. This is a function of the actual distance - * that needs to be traveled; we keep this value close to half screen - * size in order to reduce the variance in snap duration as a function - * of the distance the page needs to travel. - */ - final float halfScreenSize = width / 2; - float distanceRatio = delta / width; - float distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio); - float distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration; - - velocity = Math.abs(velocity); - velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity); - - /* - * we want the page's snap velocity to approximately match the velocity - * at which the user flings, so we scale the duration by a value near to - * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to - * make it a little slower. - */ - long duration = 6 * Math.round(1000 * Math.abs(distance / velocity)); - if (DEBUG) { - Log.e(TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:" - + distanceRatio + " distance:" + distance + " velocity:" + velocity - + " duration:" + duration + " distanceInfluenceForSnapDuration:" - + distanceInfluenceForSnapDuration); - } - return duration; - } - - /* - * We want the duration of the page snap animation to be influenced by the - * distance that the screen has to travel, however, we don't want this - * duration to be effected in a purely linear fashion. Instead, we use this - * method to moderate the effect that the distance of travel has on the - * overall snap duration. - */ - private float distanceInfluenceForSnapDuration(float f) { - f -= 0.5f; // center the values about 0. - f *= 0.3f * Math.PI / 2.0f; - return (float) Math.sin(f); - } -} diff --git a/src/com/android/calendar/DayView.kt b/src/com/android/calendar/DayView.kt new file mode 100644 index 00000000..42621638 --- /dev/null +++ b/src/com/android/calendar/DayView.kt @@ -0,0 +1,3990 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.app.Service +import android.content.Context +import android.content.res.Resources +import android.content.res.TypedArray +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Paint.Align +import android.graphics.Paint.Style +import android.graphics.Rect +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.os.Handler +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Calendars +import android.text.Layout.Alignment +import android.text.SpannableStringBuilder +import android.text.StaticLayout +import android.text.TextPaint +import android.text.format.DateFormat +import android.text.format.DateUtils +import android.text.format.Time +import android.text.style.StyleSpan +import android.util.Log +import android.view.ContextMenu +import android.view.ContextMenu.ContextMenuInfo +import android.view.GestureDetector +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import android.view.WindowManager +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityManager +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.Animation +import android.view.animation.Interpolator +import android.view.animation.TranslateAnimation +import android.widget.EdgeEffect +import android.widget.OverScroller +import android.widget.PopupWindow +import android.widget.ViewSwitcher +import com.android.calendar.CalendarController.EventType +import com.android.calendar.CalendarController.ViewType +import java.util.ArrayList +import java.util.Arrays +import java.util.Calendar +import java.util.Formatter +import java.util.Locale +import java.util.regex.Matcher +import java.util.regex.Pattern + +/** + * View for multi-day view. So far only 1 and 7 day have been tested. + */ +class DayView( + context: Context?, + controller: CalendarController?, + viewSwitcher: ViewSwitcher?, + eventLoader: EventLoader?, + numDays: Int +) : View(context), View.OnCreateContextMenuListener, ScaleGestureDetector.OnScaleGestureListener, + View.OnClickListener, View.OnLongClickListener { + private var mOnFlingCalled = false + private var mStartingScroll = false + protected var mPaused = true + private var mHandler: Handler? = null + + /** + * ID of the last event which was displayed with the toast popup. + * + * This is used to prevent popping up multiple quick views for the same event, especially + * during calendar syncs. This becomes valid when an event is selected, either by default + * on starting calendar or by scrolling to an event. It becomes invalid when the user + * explicitly scrolls to an empty time slot, changes views, or deletes the event. + */ + private var mLastPopupEventID: Long + protected var mContext: Context? = null + private val mContinueScroll: ContinueScroll = ContinueScroll() + + // Make this visible within the package for more informative debugging + var mBaseDate: Time? = null + private var mCurrentTime: Time? = null + private val mUpdateCurrentTime: UpdateCurrentTime = UpdateCurrentTime() + private var mTodayJulianDay = 0 + private val mBold: Typeface = Typeface.DEFAULT_BOLD + private var mFirstJulianDay = 0 + private var mLoadedFirstJulianDay = -1 + private var mLastJulianDay = 0 + private var mMonthLength = 0 + private var mFirstVisibleDate = 0 + private var mFirstVisibleDayOfWeek = 0 + private var mEarliestStartHour: IntArray? = null // indexed by the week day offset + private var mHasAllDayEvent: BooleanArray? = null // indexed by the week day offset + private var mEventCountTemplate: String? = null + private var mClickedEvent: Event? = null // The event the user clicked on + private var mSavedClickedEvent: Event? = null + private var mClickedYLocation = 0 + private var mDownTouchTime: Long = 0 + private var mEventsAlpha = 255 + private var mEventsCrossFadeAnimation: ObjectAnimator? = null + private val mTZUpdater: Runnable = object : Runnable { + @Override + override fun run() { + val tz: String? = Utils.getTimeZone(mContext, this) + mBaseDate!!.timezone = tz + mBaseDate?.normalize(true) + mCurrentTime?.switchTimezone(tz) + invalidate() + } + } + + // Sets the "clicked" color from the clicked event + private val mSetClick: Runnable = object : Runnable { + @Override + override fun run() { + mClickedEvent = mSavedClickedEvent + mSavedClickedEvent = null + this@DayView.invalidate() + } + } + + // Clears the "clicked" color from the clicked event and launch the event + private val mClearClick: Runnable = object : Runnable { + @Override + override fun run() { + if (mClickedEvent != null) { + mController?.sendEventRelatedEvent( + this as Object?, EventType.VIEW_EVENT, mClickedEvent!!.id, + mClickedEvent!!.startMillis, mClickedEvent!!.endMillis, + this@DayView.getWidth() / 2, mClickedYLocation, + selectedTimeInMillis + ) + } + mClickedEvent = null + this@DayView.invalidate() + } + } + private val mTodayAnimatorListener: TodayAnimatorListener = TodayAnimatorListener() + + internal inner class TodayAnimatorListener : AnimatorListenerAdapter() { + @Volatile + private var mAnimator: Animator? = null + + @Volatile + private var mFadingIn = false + @Override + override fun onAnimationEnd(animation: Animator) { + synchronized(this) { + if (mAnimator !== animation) { + animation.removeAllListeners() + animation.cancel() + return + } + if (mFadingIn) { + if (mTodayAnimator != null) { + mTodayAnimator?.removeAllListeners() + mTodayAnimator?.cancel() + } + mTodayAnimator = ObjectAnimator + .ofInt(this@DayView, "animateTodayAlpha", 255, 0) + mAnimator = mTodayAnimator + mFadingIn = false + mTodayAnimator?.addListener(this) + mTodayAnimator?.setDuration(600) + mTodayAnimator?.start() + } else { + mAnimateToday = false + mAnimateTodayAlpha = 0 + mAnimator?.removeAllListeners() + mAnimator = null + mTodayAnimator = null + invalidate() + } + } + } + + fun setAnimator(animation: Animator?) { + mAnimator = animation + } + + fun setFadingIn(fadingIn: Boolean) { + mFadingIn = fadingIn + } + } + + var mAnimatorListener: AnimatorListenerAdapter = object : AnimatorListenerAdapter() { + @Override + override fun onAnimationStart(animation: Animator) { + mScrolling = true + } + + @Override + override fun onAnimationCancel(animation: Animator) { + mScrolling = false + } + + @Override + override fun onAnimationEnd(animation: Animator) { + mScrolling = false + resetSelectedHour() + invalidate() + } + } + + /** + * This variable helps to avoid unnecessarily reloading events by keeping + * track of the start millis parameter used for the most recent loading + * of events. If the next reload matches this, then the events are not + * reloaded. To force a reload, set this to zero (this is set to zero + * in the method clearCachedEvents()). + */ + private var mLastReloadMillis: Long = 0 + private var mEvents: ArrayList<Event> = ArrayList<Event>() + private var mAllDayEvents: ArrayList<Event>? = ArrayList<Event>() + private var mLayouts: Array<StaticLayout?>? = null + private var mAllDayLayouts: Array<StaticLayout?>? = null + private var mSelectionDay = 0 // Julian day + private var mSelectionHour = 0 + var mSelectionAllday = false + + // Current selection info for accessibility + private var mSelectionDayForAccessibility = 0 // Julian day + private var mSelectionHourForAccessibility = 0 + private var mSelectedEventForAccessibility: Event? = null + + // Last selection info for accessibility + private var mLastSelectionDayForAccessibility = 0 + private var mLastSelectionHourForAccessibility = 0 + private var mLastSelectedEventForAccessibility: Event? = null + + /** Width of a day or non-conflicting event */ + private var mCellWidth = 0 + + // Pre-allocate these objects and re-use them + private val mRect: Rect = Rect() + private val mDestRect: Rect = Rect() + private val mSelectionRect: Rect = Rect() + + // This encloses the more allDay events icon + private val mExpandAllDayRect: Rect = Rect() + + // TODO Clean up paint usage + private val mPaint: Paint = Paint() + private val mEventTextPaint: Paint = Paint() + private val mSelectionPaint: Paint = Paint() + private var mLines: FloatArray = emptyArray<Float>().toFloatArray() + private var mFirstDayOfWeek = 0 // First day of the week + private var mPopup: PopupWindow? = null + private var mPopupView: View? = null + private val mDismissPopup: DismissPopup = DismissPopup() + private var mRemeasure = true + private val mEventLoader: EventLoader + protected val mEventGeometry: EventGeometry + private var mAnimationDistance = 0f + private var mViewStartX = 0 + private var mViewStartY = 0 + private var mMaxViewStartY = 0 + private var mViewHeight = 0 + private var mViewWidth = 0 + private var mGridAreaHeight = -1 + private var mScrollStartY = 0 + private var mPreviousDirection = 0 + + /** + * Vertical distance or span between the two touch points at the start of a + * scaling gesture + */ + private var mStartingSpanY = 0f + + /** Height of 1 hour in pixels at the start of a scaling gesture */ + private var mCellHeightBeforeScaleGesture = 0 + + /** The hour at the center two touch points */ + private var mGestureCenterHour = 0f + private var mRecalCenterHour = false + + /** + * Flag to decide whether to handle the up event. Cases where up events + * should be ignored are 1) right after a scale gesture and 2) finger was + * down before app launch + */ + private var mHandleActionUp = true + private var mHoursTextHeight = 0 + + /** + * The height of the area used for allday events + */ + private var mAlldayHeight = 0 + + /** + * The height of the allday event area used during animation + */ + private var mAnimateDayHeight = 0 + + /** + * The height of an individual allday event during animation + */ + private var mAnimateDayEventHeight = MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt() + + /** + * Max of all day events in a given day in this view. + */ + private var mMaxAlldayEvents = 0 + + /** + * A count of the number of allday events that were not drawn for each day + */ + private var mSkippedAlldayEvents: IntArray? = null + + /** + * The number of allDay events at which point we start hiding allDay events. + */ + private var mMaxUnexpandedAlldayEventCount = 4 + protected var mNumDays = 7 + private var mNumHours = 10 + + /** Width of the time line (list of hours) to the left. */ + private var mHoursWidth = 0 + private var mDateStrWidth = 0 + + /** Top of the scrollable region i.e. below date labels and all day events */ + private var mFirstCell = 0 + + /** First fully visible hour */ + private var mFirstHour = -1 + + /** Distance between the mFirstCell and the top of first fully visible hour. */ + private var mFirstHourOffset = 0 + private var mHourStrs: Array<String>? = null + private var mDayStrs: Array<String?>? = null + private var mDayStrs2Letter: Array<String?>? = null + private var mIs24HourFormat = false + private val mSelectedEvents: ArrayList<Event> = ArrayList<Event>() + private var mComputeSelectedEvents = false + private var mUpdateToast = false + private var mSelectedEvent: Event? = null + private var mPrevSelectedEvent: Event? = null + private val mPrevBox: Rect = Rect() + protected val mResources: Resources + protected val mCurrentTimeLine: Drawable + protected val mCurrentTimeAnimateLine: Drawable + protected val mTodayHeaderDrawable: Drawable + protected val mExpandAlldayDrawable: Drawable + protected val mCollapseAlldayDrawable: Drawable + protected var mAcceptedOrTentativeEventBoxDrawable: Drawable + private var mAmString: String? = null + private var mPmString: String? = null + var mScaleGestureDetector: ScaleGestureDetector + private var mTouchMode = TOUCH_MODE_INITIAL_STATE + private var mSelectionMode = SELECTION_HIDDEN + private var mScrolling = false + + // Pixels scrolled + private var mInitialScrollX = 0f + private var mInitialScrollY = 0f + private var mAnimateToday = false + private var mAnimateTodayAlpha = 0 + + // Animates the height of the allday region + var mAlldayAnimator: ObjectAnimator? = null + + // Animates the height of events in the allday region + var mAlldayEventAnimator: ObjectAnimator? = null + + // Animates the transparency of the more events text + var mMoreAlldayEventsAnimator: ObjectAnimator? = null + + // Animates the current time marker when Today is pressed + var mTodayAnimator: ObjectAnimator? = null + + // whether or not an event is stopping because it was cancelled + private var mCancellingAnimations = false + + // tracks whether a touch originated in the allday area + private var mTouchStartedInAlldayArea = false + private val mController: CalendarController + private val mViewSwitcher: ViewSwitcher + private val mGestureDetector: GestureDetector + private val mScroller: OverScroller + private val mEdgeEffectTop: EdgeEffect + private val mEdgeEffectBottom: EdgeEffect + private var mCallEdgeEffectOnAbsorb = false + private val OVERFLING_DISTANCE: Int + private var mLastVelocity = 0f + private val mHScrollInterpolator: ScrollInterpolator + private var mAccessibilityMgr: AccessibilityManager? = null + private var mIsAccessibilityEnabled = false + private var mTouchExplorationEnabled = false + private val mNewEventHintString: String + @Override + protected override fun onAttachedToWindow() { + if (mHandler == null) { + mHandler = getHandler() + mHandler?.post(mUpdateCurrentTime) + } + } + + private fun init(context: Context) { + setFocusable(true) + + // Allow focus in touch mode so that we can do keyboard shortcuts + // even after we've entered touch mode. + setFocusableInTouchMode(true) + setClickable(true) + setOnCreateContextMenuListener(this) + mFirstDayOfWeek = Utils.getFirstDayOfWeek(context) + mCurrentTime = Time(Utils.getTimeZone(context, mTZUpdater)) + val currentTime: Long = System.currentTimeMillis() + mCurrentTime?.set(currentTime) + mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime!!.gmtoff) + mWeek_saturdayColor = mResources.getColor(R.color.week_saturday) + mWeek_sundayColor = mResources.getColor(R.color.week_sunday) + mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color) + mFutureBgColorRes = mResources.getColor(R.color.calendar_future_bg_color) + mBgColor = mResources.getColor(R.color.calendar_hour_background) + mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label) + mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected) + mCalendarGridLineInnerHorizontalColor = mResources + .getColor(R.color.calendar_grid_line_inner_horizontal_color) + mCalendarGridLineInnerVerticalColor = mResources + .getColor(R.color.calendar_grid_line_inner_vertical_color) + mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label) + mEventTextColor = mResources.getColor(R.color.calendar_event_text_color) + mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color) + mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE) + mEventTextPaint.setTextAlign(Paint.Align.LEFT) + mEventTextPaint.setAntiAlias(true) + val gridLineColor: Int = mResources.getColor(R.color.calendar_grid_line_highlight_color) + var p: Paint = mSelectionPaint + p.setColor(gridLineColor) + p.setStyle(Style.FILL) + p.setAntiAlias(false) + p = mPaint + p.setAntiAlias(true) + + // Allocate space for 2 weeks worth of weekday names so that we can + // easily start the week display at any week day. + mDayStrs = arrayOfNulls(14) + + // Also create an array of 2-letter abbreviations. + mDayStrs2Letter = arrayOfNulls(14) + for (i in Calendar.SUNDAY..Calendar.SATURDAY) { + val index: Int = i - Calendar.SUNDAY + // e.g. Tue for Tuesday + mDayStrs!![index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM) + .toUpperCase() + mDayStrs!![index + 7] = mDayStrs!![index] + // e.g. Tu for Tuesday + mDayStrs2Letter!![index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT) + .toUpperCase() + + // If we don't have 2-letter day strings, fall back to 1-letter. + if (mDayStrs2Letter!![index]!!.equals(mDayStrs!![index])) { + mDayStrs2Letter!![index] = DateUtils.getDayOfWeekString(i, + DateUtils.LENGTH_SHORTEST) + } + mDayStrs2Letter!![index + 7] = mDayStrs2Letter!![index] + } + + // Figure out how much space we need for the 3-letter abbrev names + // in the worst case. + p.setTextSize(DATE_HEADER_FONT_SIZE) + p.setTypeface(mBold) + val dateStrs = arrayOf<String?>(" 28", " 30") + mDateStrWidth = computeMaxStringWidth(0, dateStrs, p) + p.setTextSize(DAY_HEADER_FONT_SIZE) + mDateStrWidth += computeMaxStringWidth(0, mDayStrs as Array<String?>, p) + p.setTextSize(HOURS_TEXT_SIZE) + p.setTypeface(null) + handleOnResume() + mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase() + mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase() + val ampm = arrayOf(mAmString, mPmString) + p.setTextSize(AMPM_TEXT_SIZE) + mHoursWidth = Math.max( + HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p) + + HOURS_RIGHT_MARGIN + ) + mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth) + val inflater: LayoutInflater + inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + mPopupView = inflater.inflate(R.layout.bubble_event, null) + mPopupView?.setLayoutParams( + LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + ) + mPopup = PopupWindow(context) + mPopup?.setContentView(mPopupView) + val dialogTheme: Resources.Theme = getResources().newTheme() + dialogTheme.applyStyle(android.R.style.Theme_Dialog, true) + val ta: TypedArray = dialogTheme.obtainStyledAttributes( + intArrayOf( + android.R.attr.windowBackground + ) + ) + mPopup?.setBackgroundDrawable(ta.getDrawable(0)) + ta.recycle() + + // Enable touching the popup window + mPopupView?.setOnClickListener(this) + // Catch long clicks for creating a new event + setOnLongClickListener(this) + mBaseDate = Time(Utils.getTimeZone(context, mTZUpdater)) + val millis: Long = System.currentTimeMillis() + mBaseDate?.set(millis) + mEarliestStartHour = IntArray(mNumDays) + mHasAllDayEvent = BooleanArray(mNumDays) + + // mLines is the array of points used with Canvas.drawLines() in + // drawGridBackground() and drawAllDayEvents(). Its size depends + // on the max number of lines that can ever be drawn by any single + // drawLines() call in either of those methods. + val maxGridLines = (24 + 1 + // max horizontal lines we might draw + (mNumDays + 1)) // max vertical lines we might draw + mLines = FloatArray(maxGridLines * 4) + } + + /** + * This is called when the popup window is pressed. + */ + override fun onClick(v: View) { + if (v === mPopupView) { + // Pretend it was a trackball click because that will always + // jump to the "View event" screen. + switchViews(true /* trackball */) + } + } + + fun handleOnResume() { + initAccessibilityVariables() + if (Utils.getSharedPreference(mContext, OtherPreferences.KEY_OTHER_1, false)) { + mFutureBgColor = 0 + } else { + mFutureBgColor = mFutureBgColorRes + } + mIs24HourFormat = DateFormat.is24HourFormat(mContext) + mHourStrs = if (mIs24HourFormat) CalendarData.s24Hours else CalendarData.s12HoursNoAmPm + mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext) + mLastSelectionDayForAccessibility = 0 + mLastSelectionHourForAccessibility = 0 + mLastSelectedEventForAccessibility = null + mSelectionMode = SELECTION_HIDDEN + } + + private fun initAccessibilityVariables() { + mAccessibilityMgr = mContext + ?.getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager + mIsAccessibilityEnabled = mAccessibilityMgr != null && mAccessibilityMgr!!.isEnabled() + mTouchExplorationEnabled = isTouchExplorationEnabled + } /* ignore isDst */ // We ignore the "isDst" field because we want normalize() to figure + // out the correct DST value and not adjust the selected time based + // on the current setting of DST. + /** + * Returns the start of the selected time in milliseconds since the epoch. + * + * @return selected time in UTC milliseconds since the epoch. + */ + val selectedTimeInMillis: Long + get() { + val time = Time(mBaseDate) + time.setJulianDay(mSelectionDay) + time.hour = mSelectionHour + + // We ignore the "isDst" field because we want normalize() to figure + // out the correct DST value and not adjust the selected time based + // on the current setting of DST. + return time.normalize(true /* ignore isDst */) + } /* ignore isDst */ + + // We ignore the "isDst" field because we want normalize() to figure + // out the correct DST value and not adjust the selected time based + // on the current setting of DST. + val selectedTime: Time + get() { + val time = Time(mBaseDate) + time.setJulianDay(mSelectionDay) + time.hour = mSelectionHour + + // We ignore the "isDst" field because we want normalize() to figure + // out the correct DST value and not adjust the selected time based + // on the current setting of DST. + time.normalize(true /* ignore isDst */) + return time + } /* ignore isDst */ + + // We ignore the "isDst" field because we want normalize() to figure + // out the correct DST value and not adjust the selected time based + // on the current setting of DST. + val selectedTimeForAccessibility: Time + get() { + val time = Time(mBaseDate) + time.setJulianDay(mSelectionDayForAccessibility) + time.hour = mSelectionHourForAccessibility + + // We ignore the "isDst" field because we want normalize() to figure + // out the correct DST value and not adjust the selected time based + // on the current setting of DST. + time.normalize(true /* ignore isDst */) + return time + } + + /** + * Returns the start of the selected time in minutes since midnight, + * local time. The derived class must ensure that this is consistent + * with the return value from getSelectedTimeInMillis(). + */ + val selectedMinutesSinceMidnight: Int + get() = mSelectionHour * MINUTES_PER_HOUR + var firstVisibleHour: Int + get() = mFirstHour + set(firstHour) { + mFirstHour = firstHour + mFirstHourOffset = 0 + } + + fun setSelected(time: Time?, ignoreTime: Boolean, animateToday: Boolean) { + mBaseDate?.set(time) + setSelectedHour(mBaseDate!!.hour) + setSelectedEvent(null) + mPrevSelectedEvent = null + val millis: Long = mBaseDate!!.toMillis(false /* use isDst */) + setSelectedDay(Time.getJulianDay(millis, mBaseDate!!.gmtoff)) + mSelectedEvents.clear() + mComputeSelectedEvents = true + var gotoY: Int = Integer.MIN_VALUE + if (!ignoreTime && mGridAreaHeight != -1) { + var lastHour = 0 + if (mBaseDate!!.hour < mFirstHour) { + // Above visible region + gotoY = mBaseDate!!.hour * (mCellHeight + HOUR_GAP) + } else { + lastHour = ((mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP) + + mFirstHour) + if (mBaseDate!!.hour >= lastHour) { + // Below visible region + + // target hour + 1 (to give it room to see the event) - + // grid height (to get the y of the top of the visible + // region) + gotoY = ((mBaseDate!!.hour + 1 + mBaseDate!!.minute / 60.0f) * + (mCellHeight + HOUR_GAP) - mGridAreaHeight).toInt() + } + } + if (DEBUG) { + Log.e( + TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH " + + (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight + + " ymax " + mMaxViewStartY + ) + } + if (gotoY > mMaxViewStartY) { + gotoY = mMaxViewStartY + } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) { + gotoY = 0 + } + } + recalc() + mRemeasure = true + invalidate() + var delayAnimateToday = false + if (gotoY != Integer.MIN_VALUE) { + val scrollAnim: ValueAnimator = + ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY) + scrollAnim.setDuration(GOTO_SCROLL_DURATION.toLong()) + scrollAnim.setInterpolator(AccelerateDecelerateInterpolator()) + scrollAnim.addListener(mAnimatorListener) + scrollAnim.start() + delayAnimateToday = true + } + if (animateToday) { + synchronized(mTodayAnimatorListener) { + if (mTodayAnimator != null) { + mTodayAnimator?.removeAllListeners() + mTodayAnimator?.cancel() + } + mTodayAnimator = ObjectAnimator.ofInt( + this, "animateTodayAlpha", + mAnimateTodayAlpha, 255 + ) + mAnimateToday = true + mTodayAnimatorListener.setFadingIn(true) + mTodayAnimatorListener.setAnimator(mTodayAnimator) + mTodayAnimator?.addListener(mTodayAnimatorListener) + mTodayAnimator?.setDuration(150) + if (delayAnimateToday) { + mTodayAnimator?.setStartDelay(GOTO_SCROLL_DURATION.toLong()) + } + mTodayAnimator?.start() + } + } + sendAccessibilityEventAsNeeded(false) + } + + // Called from animation framework via reflection. Do not remove + fun setViewStartY(viewStartY: Int) { + var viewStartY = viewStartY + if (viewStartY > mMaxViewStartY) { + viewStartY = mMaxViewStartY + } + mViewStartY = viewStartY + computeFirstHour() + invalidate() + } + + fun setAnimateTodayAlpha(todayAlpha: Int) { + mAnimateTodayAlpha = todayAlpha + invalidate() + } /* ignore isDst */ + + fun getSelectedDay(): Time { + val time = Time(mBaseDate) + time.setJulianDay(mSelectionDay) + time.hour = mSelectionHour + + // We ignore the "isDst" field because we want normalize() to figure + // out the correct DST value and not adjust the selected time based + // on the current setting of DST. + time.normalize(true /* ignore isDst */) + return time + } + + fun updateTitle() { + val start = Time(mBaseDate) + start.normalize(true) + val end = Time(start) + end.monthDay += mNumDays - 1 + // Move it forward one minute so the formatter doesn't lose a day + end.minute += 1 + end.normalize(true) + var formatFlags: Long = DateUtils.FORMAT_SHOW_DATE.toLong() or + DateUtils.FORMAT_SHOW_YEAR.toLong() + if (mNumDays != 1) { + // Don't show day of the month if for multi-day view + formatFlags = formatFlags or DateUtils.FORMAT_NO_MONTH_DAY.toLong() + + // Abbreviate the month if showing multiple months + if (start.month !== end.month) { + formatFlags = formatFlags or DateUtils.FORMAT_ABBREV_MONTH.toLong() + } + } + mController.sendEvent( + this as Object?, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT, + formatFlags, null, null + ) + } + + /** + * return a negative number if "time" is comes before the visible time + * range, a positive number if "time" is after the visible time range, and 0 + * if it is in the visible time range. + */ + fun compareToVisibleTimeRange(time: Time): Int { + val savedHour: Int = mBaseDate!!.hour + val savedMinute: Int = mBaseDate!!.minute + val savedSec: Int = mBaseDate!!.second + mBaseDate!!.hour = 0 + mBaseDate!!.minute = 0 + mBaseDate!!.second = 0 + if (DEBUG) { + Log.d(TAG, "Begin " + mBaseDate.toString()) + Log.d(TAG, "Diff " + time.toString()) + } + + // Compare beginning of range + var diff: Int = Time.compare(time, mBaseDate) + if (diff > 0) { + // Compare end of range + mBaseDate!!.monthDay += mNumDays + mBaseDate?.normalize(true) + diff = Time.compare(time, mBaseDate) + if (DEBUG) Log.d(TAG, "End " + mBaseDate.toString()) + mBaseDate!!.monthDay -= mNumDays + mBaseDate?.normalize(true) + if (diff < 0) { + // in visible time + diff = 0 + } else if (diff == 0) { + // Midnight of following day + diff = 1 + } + } + if (DEBUG) Log.d(TAG, "Diff: $diff") + mBaseDate!!.hour = savedHour + mBaseDate!!.minute = savedMinute + mBaseDate!!.second = savedSec + return diff + } + + private fun recalc() { + // Set the base date to the beginning of the week if we are displaying + // 7 days at a time. + if (mNumDays == 7) { + adjustToBeginningOfWeek(mBaseDate) + } + val start: Long = mBaseDate!!.toMillis(false /* use isDst */) + mFirstJulianDay = Time.getJulianDay(start, mBaseDate!!.gmtoff) + mLastJulianDay = mFirstJulianDay + mNumDays - 1 + mMonthLength = mBaseDate!!.getActualMaximum(Time.MONTH_DAY) + mFirstVisibleDate = mBaseDate!!.monthDay + mFirstVisibleDayOfWeek = mBaseDate!!.weekDay + } + + private fun adjustToBeginningOfWeek(time: Time?) { + val dayOfWeek: Int = time!!.weekDay + var diff = dayOfWeek - mFirstDayOfWeek + if (diff != 0) { + if (diff < 0) { + diff += 7 + } + time!!.monthDay -= diff + time?.normalize(true /* ignore isDst */) + } + } + + @Override + protected override fun onSizeChanged(width: Int, height: Int, oldw: Int, oldh: Int) { + mViewWidth = width + mViewHeight = height + mEdgeEffectTop.setSize(mViewWidth, mViewHeight) + mEdgeEffectBottom.setSize(mViewWidth, mViewHeight) + val gridAreaWidth = width - mHoursWidth + mCellWidth = (gridAreaWidth - mNumDays * DAY_GAP) / mNumDays + + // This would be about 1 day worth in a 7 day view + mHorizontalSnapBackThreshold = width / 7 + val p = Paint() + p.setTextSize(HOURS_TEXT_SIZE) + mHoursTextHeight = Math.abs(p.ascent()).toInt() + remeasure(width, height) + } + + /** + * Measures the space needed for various parts of the view after + * loading new events. This can change if there are all-day events. + */ + private fun remeasure(width: Int, height: Int) { + // Shrink to fit available space but make sure we can display at least two events + MAX_UNEXPANDED_ALLDAY_HEIGHT = (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4).toInt() + MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6) + MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max( + MAX_UNEXPANDED_ALLDAY_HEIGHT, + MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt() * 2 + ) + mMaxUnexpandedAlldayEventCount = + (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt() + + // First, clear the array of earliest start times, and the array + // indicating presence of an all-day event. + for (day in 0 until mNumDays) { + mEarliestStartHour!![day] = 25 // some big number + mHasAllDayEvent!![day] = false + } + val maxAllDayEvents = mMaxAlldayEvents + + // The min is where 24 hours cover the entire visible area + mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, MIN_EVENT_HEIGHT.toInt()) + if (mCellHeight < mMinCellHeight) { + mCellHeight = mMinCellHeight + } + + // Calculate mAllDayHeight + mFirstCell = DAY_HEADER_HEIGHT + var allDayHeight = 0 + if (maxAllDayEvents > 0) { + val maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT + // If there is at most one all-day event per day, then use less + // space (but more than the space for a single event). + if (maxAllDayEvents == 1) { + allDayHeight = SINGLE_ALLDAY_HEIGHT + } else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount) { + // Allow the all-day area to grow in height depending on the + // number of all-day events we need to show, up to a limit. + allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT + if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { + allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT + } + } else { + // if we have more than the magic number, check if we're animating + // and if not adjust the sizes appropriately + if (mAnimateDayHeight != 0) { + // Don't shrink the space past the final allDay space. The animation + // continues to hide the last event so the more events text can + // fade in. + allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT) + } else { + // Try to fit all the events in + allDayHeight = (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt() + // But clip the area depending on which mode we're in + if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { + allDayHeight = (mMaxUnexpandedAlldayEventCount * + MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt() + } else if (allDayHeight > maxAllAllDayHeight) { + allDayHeight = maxAllAllDayHeight + } + } + } + mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN + } else { + mSelectionAllday = false + } + mAlldayHeight = allDayHeight + mGridAreaHeight = height - mFirstCell + + // Set up the expand icon position + val allDayIconWidth: Int = mExpandAlldayDrawable.getIntrinsicWidth() + mExpandAllDayRect.left = Math.max( + (mHoursWidth - allDayIconWidth) / 2, + EVENT_ALL_DAY_TEXT_LEFT_MARGIN + ) + mExpandAllDayRect.right = Math.min( + mExpandAllDayRect.left + allDayIconWidth, mHoursWidth - + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN + ) + mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN + mExpandAllDayRect.top = (mExpandAllDayRect.bottom - + mExpandAlldayDrawable.getIntrinsicHeight()) + mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP) + mEventGeometry.setHourHeight(mCellHeight.toFloat()) + val minimumDurationMillis = + (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)).toLong() + Event.computePositions(mEvents, minimumDurationMillis) + + // Compute the top of our reachable view + mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight + if (DEBUG) { + Log.e(TAG, "mViewStartY: $mViewStartY") + Log.e(TAG, "mMaxViewStartY: $mMaxViewStartY") + } + if (mViewStartY > mMaxViewStartY) { + mViewStartY = mMaxViewStartY + computeFirstHour() + } + if (mFirstHour == -1) { + initFirstHour() + mFirstHourOffset = 0 + } + + // When we change the base date, the number of all-day events may + // change and that changes the cell height. When we switch dates, + // we use the mFirstHourOffset from the previous view, but that may + // be too large for the new view if the cell height is smaller. + if (mFirstHourOffset >= mCellHeight + HOUR_GAP) { + mFirstHourOffset = mCellHeight + HOUR_GAP - 1 + } + mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset + val eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP) + // When we get new events we don't want to dismiss the popup unless the event changes + if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent!!.id) { + mPopup?.dismiss() + } + mPopup?.setWidth(eventAreaWidth - 20) + mPopup?.setHeight(WindowManager.LayoutParams.WRAP_CONTENT) + } + + /** + * Initialize the state for another view. The given view is one that has + * its own bitmap and will use an animation to replace the current view. + * The current view and new view are either both Week views or both Day + * views. They differ in their base date. + * + * @param view the view to initialize. + */ + private fun initView(view: DayView) { + view.setSelectedHour(mSelectionHour) + view.mSelectedEvents.clear() + view.mComputeSelectedEvents = true + view.mFirstHour = mFirstHour + view.mFirstHourOffset = mFirstHourOffset + view.remeasure(getWidth(), getHeight()) + view.initAllDayHeights() + view.setSelectedEvent(null) + view.mPrevSelectedEvent = null + view.mFirstDayOfWeek = mFirstDayOfWeek + if (view.mEvents.size > 0) { + view.mSelectionAllday = mSelectionAllday + } else { + view.mSelectionAllday = false + } + + // Redraw the screen so that the selection box will be redrawn. We may + // have scrolled to a different part of the day in some other view + // so the selection box in this view may no longer be visible. + view.recalc() + } + + /** + * Switch to another view based on what was selected (an event or a free + * slot) and how it was selected (by touch or by trackball). + * + * @param trackBallSelection true if the selection was made using the + * trackball. + */ + private fun switchViews(trackBallSelection: Boolean) { + val selectedEvent: Event? = mSelectedEvent + mPopup?.dismiss() + mLastPopupEventID = INVALID_EVENT_ID + if (mNumDays > 1) { + // This is the Week view. + // With touch, we always switch to Day/Agenda View + // With track ball, if we selected a free slot, then create an event. + // If we selected a specific event, switch to EventInfo view. + if (trackBallSelection) { + if (selectedEvent != null) { + if (mIsAccessibilityEnabled) { + mAccessibilityMgr?.interrupt() + } + } + } + } + } + + @Override + override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + mScrolling = false + return super.onKeyUp(keyCode, event) + } + + @Override + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + return super.onKeyDown(keyCode, event) + } + + @Override + override fun onHoverEvent(event: MotionEvent?): Boolean { + return true + } + + private val isTouchExplorationEnabled: Boolean + private get() = mIsAccessibilityEnabled && mAccessibilityMgr!!.isTouchExplorationEnabled() + + private fun sendAccessibilityEventAsNeeded(speakEvents: Boolean) { + if (!mIsAccessibilityEnabled) { + return + } + val dayChanged = mLastSelectionDayForAccessibility != mSelectionDayForAccessibility + val hourChanged = mLastSelectionHourForAccessibility != mSelectionHourForAccessibility + if (dayChanged || hourChanged || mLastSelectedEventForAccessibility !== + mSelectedEventForAccessibility) { + mLastSelectionDayForAccessibility = mSelectionDayForAccessibility + mLastSelectionHourForAccessibility = mSelectionHourForAccessibility + mLastSelectedEventForAccessibility = mSelectedEventForAccessibility + val b = StringBuilder() + + // Announce only the changes i.e. day or hour or both + if (dayChanged) { + b.append(selectedTimeForAccessibility.format("%A ")) + } + if (hourChanged) { + b.append(selectedTimeForAccessibility.format(if (mIs24HourFormat) "%k" else "%l%p")) + } + if (dayChanged || hourChanged) { + b.append(PERIOD_SPACE) + } + if (speakEvents) { + if (mEventCountTemplate == null) { + mEventCountTemplate = mContext?.getString(R.string.template_announce_item_index) + } + + // Read out the relevant event(s) + val numEvents: Int = mSelectedEvents.size + if (numEvents > 0) { + if (mSelectedEventForAccessibility == null) { + // Read out all the events + var i = 1 + for (calEvent in mSelectedEvents) { + if (numEvents > 1) { + // Read out x of numEvents if there are more than one event + mStringBuilder.setLength(0) + b.append(mFormatter.format(mEventCountTemplate, i++, numEvents)) + b.append(" ") + } + appendEventAccessibilityString(b, calEvent) + } + } else { + if (numEvents > 1) { + // Read out x of numEvents if there are more than one event + mStringBuilder.setLength(0) + b.append( + mFormatter.format( + mEventCountTemplate, mSelectedEvents + .indexOf(mSelectedEventForAccessibility) + 1, numEvents + ) + ) + b.append(" ") + } + appendEventAccessibilityString(b, mSelectedEventForAccessibility) + } + } + } + if (dayChanged || hourChanged || speakEvents) { + val event: AccessibilityEvent = AccessibilityEvent + .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED) + val msg: CharSequence = b.toString() + event.getText().add(msg) + event.setAddedCount(msg.length) + sendAccessibilityEventUnchecked(event) + } + } + } + + /** + * @param b + * @param calEvent + */ + private fun appendEventAccessibilityString(b: StringBuilder, calEvent: Event?) { + b.append(calEvent!!.titleAndLocation) + b.append(PERIOD_SPACE) + val `when`: String? + var flags: Int = DateUtils.FORMAT_SHOW_DATE + if (calEvent!!.allDay) { + flags = flags or (DateUtils.FORMAT_UTC or DateUtils.FORMAT_SHOW_WEEKDAY) + } else { + flags = flags or DateUtils.FORMAT_SHOW_TIME + if (DateFormat.is24HourFormat(mContext)) { + flags = flags or DateUtils.FORMAT_24HOUR + } + } + `when` = Utils.formatDateRange(mContext, calEvent!!.startMillis, calEvent!!.endMillis, + flags) + b.append(`when`) + b.append(PERIOD_SPACE) + } + + private inner class GotoBroadcaster(start: Time, end: Time) : Animation.AnimationListener { + private val mCounter: Int + private val mStart: Time + private val mEnd: Time + @Override + override fun onAnimationEnd(animation: Animation) { + var view = mViewSwitcher.getCurrentView() as DayView + view.mViewStartX = 0 + view = mViewSwitcher.getNextView() as DayView + view.mViewStartX = 0 + if (mCounter == sCounter) { + mController.sendEvent( + this as Object?, EventType.GO_TO, mStart, mEnd, null, -1, + ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null + ) + } + } + + @Override + override fun onAnimationRepeat(animation: Animation) { + } + + @Override + override fun onAnimationStart(animation: Animation) { + } + + init { + mCounter = ++sCounter + mStart = start + mEnd = end + } + } + + private fun switchViews(forward: Boolean, xOffSet: Float, width: Float, velocity: Float): View { + mAnimationDistance = width - xOffSet + if (DEBUG) { + Log.d(TAG, "switchViews($forward) O:$xOffSet Dist:$mAnimationDistance") + } + var progress: Float = Math.abs(xOffSet) / width + if (progress > 1.0f) { + progress = 1.0f + } + val inFromXValue: Float + val inToXValue: Float + val outFromXValue: Float + val outToXValue: Float + if (forward) { + inFromXValue = 1.0f - progress + inToXValue = 0.0f + outFromXValue = -progress + outToXValue = -1.0f + } else { + inFromXValue = progress - 1.0f + inToXValue = 0.0f + outFromXValue = progress + outToXValue = 1.0f + } + val start = Time(mBaseDate!!.timezone) + start.set(mController.time as Long) + if (forward) { + start.monthDay += mNumDays + } else { + start.monthDay -= mNumDays + } + mController.time = start.normalize(true) + var newSelected: Time? = start + if (mNumDays == 7) { + newSelected = Time(start) + adjustToBeginningOfWeek(start) + } + val end = Time(start) + end.monthDay += mNumDays - 1 + + // We have to allocate these animation objects each time we switch views + // because that is the only way to set the animation parameters. + val inAnimation = TranslateAnimation( + Animation.RELATIVE_TO_SELF, inFromXValue, + Animation.RELATIVE_TO_SELF, inToXValue, + Animation.ABSOLUTE, 0.0f, + Animation.ABSOLUTE, 0.0f + ) + val outAnimation = TranslateAnimation( + Animation.RELATIVE_TO_SELF, outFromXValue, + Animation.RELATIVE_TO_SELF, outToXValue, + Animation.ABSOLUTE, 0.0f, + Animation.ABSOLUTE, 0.0f + ) + val duration = calculateDuration(width - Math.abs(xOffSet), width, velocity) + inAnimation.setDuration(duration) + inAnimation.setInterpolator(mHScrollInterpolator) + outAnimation.setInterpolator(mHScrollInterpolator) + outAnimation.setDuration(duration) + outAnimation.setAnimationListener(GotoBroadcaster(start, end)) + mViewSwitcher.setInAnimation(inAnimation) + mViewSwitcher.setOutAnimation(outAnimation) + var view = mViewSwitcher.getCurrentView() as DayView + view.cleanup() + mViewSwitcher.showNext() + view = mViewSwitcher.getCurrentView() as DayView + view.setSelected(newSelected, true, false) + view.requestFocus() + view.reloadEvents() + view.updateTitle() + view.restartCurrentTimeUpdates() + return view + } + + // This is called after scrolling stops to move the selected hour + // to the visible part of the screen. + private fun resetSelectedHour() { + if (mSelectionHour < mFirstHour + 1) { + setSelectedHour(mFirstHour + 1) + setSelectedEvent(null) + mSelectedEvents.clear() + mComputeSelectedEvents = true + } else if (mSelectionHour > mFirstHour + mNumHours - 3) { + setSelectedHour(mFirstHour + mNumHours - 3) + setSelectedEvent(null) + mSelectedEvents.clear() + mComputeSelectedEvents = true + } + } + + private fun initFirstHour() { + mFirstHour = mSelectionHour - mNumHours / 5 + if (mFirstHour < 0) { + mFirstHour = 0 + } else if (mFirstHour + mNumHours > 24) { + mFirstHour = 24 - mNumHours + } + } + + /** + * Recomputes the first full hour that is visible on screen after the + * screen is scrolled. + */ + private fun computeFirstHour() { + // Compute the first full hour that is visible on screen + mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP) + mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY + } + + private fun adjustHourSelection() { + if (mSelectionHour < 0) { + setSelectedHour(0) + if (mMaxAlldayEvents > 0) { + mPrevSelectedEvent = null + mSelectionAllday = true + } + } + if (mSelectionHour > 23) { + setSelectedHour(23) + } + + // If the selected hour is at least 2 time slots from the top and + // bottom of the screen, then don't scroll the view. + if (mSelectionHour < mFirstHour + 1) { + // If there are all-days events for the selected day but there + // are no more normal events earlier in the day, then jump to + // the all-day event area. + // Exception 1: allow the user to scroll to 8am with the trackball + // before jumping to the all-day event area. + // Exception 2: if 12am is on screen, then allow the user to select + // 12am before going up to the all-day event area. + val daynum = mSelectionDay - mFirstJulianDay + if (daynum < mEarliestStartHour!!.size && daynum >= 0 && mMaxAlldayEvents > 0 && + mEarliestStartHour!![daynum] > mSelectionHour && + mFirstHour > 0 && mFirstHour < 8) { + mPrevSelectedEvent = null + mSelectionAllday = true + setSelectedHour(mFirstHour + 1) + return + } + if (mFirstHour > 0) { + mFirstHour -= 1 + mViewStartY -= mCellHeight + HOUR_GAP + if (mViewStartY < 0) { + mViewStartY = 0 + } + return + } + } + if (mSelectionHour > mFirstHour + mNumHours - 3) { + if (mFirstHour < 24 - mNumHours) { + mFirstHour += 1 + mViewStartY += mCellHeight + HOUR_GAP + if (mViewStartY > mMaxViewStartY) { + mViewStartY = mMaxViewStartY + } + return + } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { + mViewStartY = mMaxViewStartY + } + } + } + + fun clearCachedEvents() { + mLastReloadMillis = 0 + } + + private val mCancelCallback: Runnable = object : Runnable { + override fun run() { + clearCachedEvents() + } + } + + /* package */ + fun reloadEvents() { + // Protect against this being called before this view has been + // initialized. +// if (mContext == null) { +// return; +// } + + // Make sure our time zones are up to date + mTZUpdater.run() + setSelectedEvent(null) + mPrevSelectedEvent = null + mSelectedEvents.clear() + + // The start date is the beginning of the week at 12am + val weekStart = Time(Utils.getTimeZone(mContext, mTZUpdater)) + weekStart.set(mBaseDate) + weekStart.hour = 0 + weekStart.minute = 0 + weekStart.second = 0 + val millis: Long = weekStart.normalize(true /* ignore isDst */) + + // Avoid reloading events unnecessarily. + if (millis == mLastReloadMillis) { + return + } + mLastReloadMillis = millis + + // load events in the background + // mContext.startProgressSpinner(); + val events: ArrayList<Event> = ArrayList<Event>() + mEventLoader.loadEventsInBackground(mNumDays, events as ArrayList<Event?>, mFirstJulianDay, + object : Runnable { + override fun run() { + val fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay + mEvents = events + mLoadedFirstJulianDay = mFirstJulianDay + if (mAllDayEvents == null) { + mAllDayEvents = ArrayList<Event>() + } else { + mAllDayEvents?.clear() + } + + // Create a shorter array for all day events + for (e in events) { + if (e.drawAsAllday()) { + mAllDayEvents?.add(e) + } + } + + // New events, new layouts + if (mLayouts == null || mLayouts!!.size < events.size) { + mLayouts = arrayOfNulls<StaticLayout>(events.size) + } else { + Arrays.fill(mLayouts, null) + } + if (mAllDayLayouts == null || mAllDayLayouts!!.size < mAllDayEvents!!.size) { + mAllDayLayouts = arrayOfNulls<StaticLayout>(events.size) + } else { + Arrays.fill(mAllDayLayouts, null) + } + computeEventRelations() + mRemeasure = true + mComputeSelectedEvents = true + recalc() + + // Start animation to cross fade the events + if (fadeinEvents) { + if (mEventsCrossFadeAnimation == null) { + mEventsCrossFadeAnimation = + ObjectAnimator.ofInt(this@DayView, "EventsAlpha", 0, 255) + mEventsCrossFadeAnimation?.setDuration(EVENTS_CROSS_FADE_DURATION.toLong()) + } + mEventsCrossFadeAnimation?.start() + } else { + invalidate() + } + } + }, mCancelCallback) + } + + var eventsAlpha: Int + get() = mEventsAlpha + set(alpha) { + mEventsAlpha = alpha + invalidate() + } + + fun stopEventsAnimation() { + if (mEventsCrossFadeAnimation != null) { + mEventsCrossFadeAnimation?.cancel() + } + mEventsAlpha = 255 + } + + private fun computeEventRelations() { + // Compute the layout relation between each event before measuring cell + // width, as the cell width should be adjusted along with the relation. + // + // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm) + // We should mark them as "overwapped". Though they are not overwapped logically, but + // minimum cell height implicitly expands the cell height of A and it should look like + // (1:00pm - 1:15pm) after the cell height adjustment. + + // Compute the space needed for the all-day events, if any. + // Make a pass over all the events, and keep track of the maximum + // number of all-day events in any one day. Also, keep track of + // the earliest event in each day. + var maxAllDayEvents = 0 + val events: ArrayList<Event> = mEvents + val len: Int = events.size + // Num of all-day-events on each day. + val eventsCount = IntArray(mLastJulianDay - mFirstJulianDay + 1) + Arrays.fill(eventsCount, 0) + for (ii in 0 until len) { + val event: Event = events.get(ii) + if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) { + continue + } + if (event.drawAsAllday()) { + // Count all the events being drawn as allDay events + val firstDay: Int = Math.max(event.startDay, mFirstJulianDay) + val lastDay: Int = Math.min(event.endDay, mLastJulianDay) + for (day in firstDay..lastDay) { + val count = ++eventsCount[day - mFirstJulianDay] + if (maxAllDayEvents < count) { + maxAllDayEvents = count + } + } + var daynum: Int = event.startDay - mFirstJulianDay + var durationDays: Int = event.endDay - event.startDay + 1 + if (daynum < 0) { + durationDays += daynum + daynum = 0 + } + if (daynum + durationDays > mNumDays) { + durationDays = mNumDays - daynum + } + var day = daynum + while (durationDays > 0) { + mHasAllDayEvent!![day] = true + day++ + durationDays-- + } + } else { + var daynum: Int = event.startDay - mFirstJulianDay + var hour: Int = event.startTime / 60 + if (daynum >= 0 && hour < mEarliestStartHour!![daynum]) { + mEarliestStartHour!![daynum] = hour + } + + // Also check the end hour in case the event spans more than + // one day. + daynum = event.endDay - mFirstJulianDay + hour = event.endTime / 60 + if (daynum < mNumDays && hour < mEarliestStartHour!![daynum]) { + mEarliestStartHour!![daynum] = hour + } + } + } + mMaxAlldayEvents = maxAllDayEvents + initAllDayHeights() + } + + @Override + protected override fun onDraw(canvas: Canvas) { + if (mRemeasure) { + remeasure(getWidth(), getHeight()) + mRemeasure = false + } + canvas.save() + val yTranslate = (-mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight).toFloat() + // offset canvas by the current drag and header position + canvas.translate(-mViewStartX.toFloat(), yTranslate) + // clip to everything below the allDay area + val dest: Rect = mDestRect + dest.top = (mFirstCell - yTranslate).toInt() + dest.bottom = (mViewHeight - yTranslate).toInt() + dest.left = 0 + dest.right = mViewWidth + canvas.save() + canvas.clipRect(dest) + // Draw the movable part of the view + doDraw(canvas) + // restore to having no clip + canvas.restore() + if (mTouchMode and TOUCH_MODE_HSCROLL != 0) { + val xTranslate: Float + xTranslate = if (mViewStartX > 0) { + mViewWidth.toFloat() + } else { + -mViewWidth.toFloat() + } + // Move the canvas around to prep it for the next view + // specifically, shift it by a screen and undo the + // yTranslation which will be redone in the nextView's onDraw(). + canvas.translate(xTranslate, -yTranslate) + val nextView = mViewSwitcher.getNextView() as DayView + + // Prevent infinite recursive calls to onDraw(). + nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE + nextView.onDraw(canvas) + // Move it back for this view + canvas.translate(-xTranslate, 0f) + } else { + // If we drew another view we already translated it back + // If we didn't draw another view we should be at the edge of the + // screen + canvas.translate(mViewStartX.toFloat(), -yTranslate) + } + + // Draw the fixed areas (that don't scroll) directly to the canvas. + drawAfterScroll(canvas) + if (mComputeSelectedEvents && mUpdateToast) { + mUpdateToast = false + } + mComputeSelectedEvents = false + + // Draw overscroll glow + if (!mEdgeEffectTop.isFinished()) { + if (DAY_HEADER_HEIGHT != 0) { + canvas.translate(0f, DAY_HEADER_HEIGHT.toFloat()) + } + if (mEdgeEffectTop.draw(canvas)) { + invalidate() + } + if (DAY_HEADER_HEIGHT != 0) { + canvas.translate(0f, -DAY_HEADER_HEIGHT.toFloat()) + } + } + if (!mEdgeEffectBottom.isFinished()) { + canvas.rotate(180f, mViewWidth.toFloat() / 2f, mViewHeight.toFloat() / 2f) + if (mEdgeEffectBottom.draw(canvas)) { + invalidate() + } + } + canvas.restore() + } + + private fun drawAfterScroll(canvas: Canvas) { + val p: Paint = mPaint + val r: Rect = mRect + drawAllDayHighlights(r, canvas, p) + if (mMaxAlldayEvents != 0) { + drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p) + drawUpperLeftCorner(r, canvas, p) + } + drawScrollLine(r, canvas, p) + drawDayHeaderLoop(r, canvas, p) + + // Draw the AM and PM indicators if we're in 12 hour mode + if (!mIs24HourFormat) { + drawAmPm(canvas, p) + } + } + + // This isn't really the upper-left corner. It's the square area just + // below the upper-left corner, above the hours and to the left of the + // all-day area. + private fun drawUpperLeftCorner(r: Rect, canvas: Canvas, p: Paint) { + setupHourTextPaint(p) + if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { + // Draw the allDay expand/collapse icon + if (mUseExpandIcon) { + mExpandAlldayDrawable.setBounds(mExpandAllDayRect) + mExpandAlldayDrawable.draw(canvas) + } else { + mCollapseAlldayDrawable.setBounds(mExpandAllDayRect) + mCollapseAlldayDrawable.draw(canvas) + } + } + } + + private fun drawScrollLine(r: Rect, canvas: Canvas, p: Paint) { + val right = computeDayLeftPosition(mNumDays) + val y = mFirstCell - 1 + p.setAntiAlias(false) + p.setStyle(Style.FILL) + p.setColor(mCalendarGridLineInnerHorizontalColor) + p.setStrokeWidth(GRID_LINE_INNER_WIDTH) + canvas.drawLine(GRID_LINE_LEFT_MARGIN, y.toFloat(), right.toFloat(), y.toFloat(), p) + p.setAntiAlias(true) + } + + // Computes the x position for the left side of the given day (base 0) + private fun computeDayLeftPosition(day: Int): Int { + val effectiveWidth = mViewWidth - mHoursWidth + return day * effectiveWidth / mNumDays + mHoursWidth + } + + private fun drawAllDayHighlights(r: Rect, canvas: Canvas, p: Paint) { + if (mFutureBgColor != 0) { + // First, color the labels area light gray + r.top = 0 + r.bottom = DAY_HEADER_HEIGHT + r.left = 0 + r.right = mViewWidth + p.setColor(mBgColor) + p.setStyle(Style.FILL) + canvas.drawRect(r, p) + // and the area that says All day + r.top = DAY_HEADER_HEIGHT + r.bottom = mFirstCell - 1 + r.left = 0 + r.right = mHoursWidth + canvas.drawRect(r, p) + var startIndex = -1 + val todayIndex = mTodayJulianDay - mFirstJulianDay + if (todayIndex < 0) { + // Future + startIndex = 0 + } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) { + // Multiday - tomorrow is visible. + startIndex = todayIndex + 1 + } + if (startIndex >= 0) { + // Draw the future highlight + r.top = 0 + r.bottom = mFirstCell - 1 + r.left = computeDayLeftPosition(startIndex) + 1 + r.right = computeDayLeftPosition(mNumDays) + p.setColor(mFutureBgColor) + p.setStyle(Style.FILL) + canvas.drawRect(r, p) + } + } + } + + private fun drawDayHeaderLoop(r: Rect, canvas: Canvas, p: Paint) { + // Draw the horizontal day background banner + // p.setColor(mCalendarDateBannerBackground); + // r.top = 0; + // r.bottom = DAY_HEADER_HEIGHT; + // r.left = 0; + // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP); + // canvas.drawRect(r, p); + // + // Fill the extra space on the right side with the default background + // r.left = r.right; + // r.right = mViewWidth; + // p.setColor(mCalendarGridAreaBackground); + // canvas.drawRect(r, p); + if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) { + return + } + p.setTypeface(mBold) + p.setTextAlign(Paint.Align.RIGHT) + var cell = mFirstJulianDay + val dayNames: Array<String?>? + dayNames = if (mDateStrWidth < mCellWidth) { + mDayStrs + } else { + mDayStrs2Letter + } + p.setAntiAlias(true) + var day = 0 + while (day < mNumDays) { + var dayOfWeek = day + mFirstVisibleDayOfWeek + if (dayOfWeek >= 14) { + dayOfWeek -= 14 + } + var color = mCalendarDateBannerTextColor + if (mNumDays == 1) { + if (dayOfWeek == Time.SATURDAY) { + color = mWeek_saturdayColor + } else if (dayOfWeek == Time.SUNDAY) { + color = mWeek_sundayColor + } + } else { + val column = day % 7 + if (Utils.isSaturday(column, mFirstDayOfWeek)) { + color = mWeek_saturdayColor + } else if (Utils.isSunday(column, mFirstDayOfWeek)) { + color = mWeek_sundayColor + } + } + p.setColor(color) + drawDayHeader(dayNames!![dayOfWeek], day, cell, canvas, p) + day++ + cell++ + } + p.setTypeface(null) + } + + private fun drawAmPm(canvas: Canvas, p: Paint) { + p.setColor(mCalendarAmPmLabel) + p.setTextSize(AMPM_TEXT_SIZE) + p.setTypeface(mBold) + p.setAntiAlias(true) + p.setTextAlign(Paint.Align.RIGHT) + var text = mAmString + if (mFirstHour >= 12) { + text = mPmString + } + var y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP + canvas.drawText(text as String, HOURS_LEFT_MARGIN.toFloat(), y.toFloat(), p) + if (mFirstHour < 12 && mFirstHour + mNumHours > 12) { + // Also draw the "PM" + text = mPmString + y = + mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) + + 2 * mHoursTextHeight + HOUR_GAP + canvas.drawText(text as String, HOURS_LEFT_MARGIN.toFloat(), y.toFloat(), p) + } + } + + private fun drawCurrentTimeLine( + r: Rect, + day: Int, + top: Int, + canvas: Canvas, + p: Paint + ) { + r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1 + r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1 + r.top = top - CURRENT_TIME_LINE_TOP_OFFSET + r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight() + mCurrentTimeLine.setBounds(r) + mCurrentTimeLine.draw(canvas) + if (mAnimateToday) { + mCurrentTimeAnimateLine.setBounds(r) + mCurrentTimeAnimateLine.setAlpha(mAnimateTodayAlpha) + mCurrentTimeAnimateLine.draw(canvas) + } + } + + private fun doDraw(canvas: Canvas) { + val p: Paint = mPaint + val r: Rect = mRect + if (mFutureBgColor != 0) { + drawBgColors(r, canvas, p) + } + drawGridBackground(r, canvas, p) + drawHours(r, canvas, p) + + // Draw each day + var cell = mFirstJulianDay + p.setAntiAlias(false) + val alpha: Int = p.getAlpha() + p.setAlpha(mEventsAlpha) + var day = 0 + while (day < mNumDays) { + + // TODO Wow, this needs cleanup. drawEvents loop through all the + // events on every call. + drawEvents(cell, day, HOUR_GAP, canvas, p) + // If this is today + if (cell == mTodayJulianDay) { + val lineY: Int = + mCurrentTime!!.hour * (mCellHeight + HOUR_GAP) + mCurrentTime!!.minute * + mCellHeight / 60 + 1 + + // And the current time shows up somewhere on the screen + if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { + drawCurrentTimeLine(r, day, lineY, canvas, p) + } + } + day++ + cell++ + } + p.setAntiAlias(true) + p.setAlpha(alpha) + } + + private fun drawHours(r: Rect, canvas: Canvas, p: Paint) { + setupHourTextPaint(p) + var y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN + for (i in 0..23) { + val time = mHourStrs!![i] + canvas.drawText(time, HOURS_LEFT_MARGIN.toFloat(), y.toFloat(), p) + y += mCellHeight + HOUR_GAP + } + } + + private fun setupHourTextPaint(p: Paint) { + p.setColor(mCalendarHourLabelColor) + p.setTextSize(HOURS_TEXT_SIZE) + p.setTypeface(Typeface.DEFAULT) + p.setTextAlign(Paint.Align.RIGHT) + p.setAntiAlias(true) + } + + private fun drawDayHeader(dayStr: String?, day: Int, cell: Int, canvas: Canvas, p: Paint) { + var dateNum = mFirstVisibleDate + day + var x: Int + if (dateNum > mMonthLength) { + dateNum -= mMonthLength + } + p.setAntiAlias(true) + val todayIndex = mTodayJulianDay - mFirstJulianDay + // Draw day of the month + val dateNumStr: String = dateNum.toString() + if (mNumDays > 1) { + val y = (DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN).toFloat() + + // Draw day of the month + x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN + p.setTextAlign(Align.RIGHT) + p.setTextSize(DATE_HEADER_FONT_SIZE) + p.setTypeface(if (todayIndex == day) mBold else Typeface.DEFAULT) + canvas.drawText(dateNumStr as String, x.toFloat(), y, p) + + // Draw day of the week + x -= (p.measureText(" $dateNumStr")).toInt() + p.setTextSize(DAY_HEADER_FONT_SIZE) + p.setTypeface(Typeface.DEFAULT) + canvas.drawText(dayStr as String, x.toFloat(), y, p) + } else { + val y = (ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN).toFloat() + p.setTextAlign(Align.LEFT) + + // Draw day of the week + x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN + p.setTextSize(DAY_HEADER_FONT_SIZE) + p.setTypeface(Typeface.DEFAULT) + canvas.drawText(dayStr as String, x.toFloat(), y, p) + + // Draw day of the month + x += (p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN).toInt() + p.setTextSize(DATE_HEADER_FONT_SIZE) + p.setTypeface(if (todayIndex == day) mBold else Typeface.DEFAULT) + canvas.drawText(dateNumStr, x.toFloat(), y, p) + } + } + + private fun drawGridBackground(r: Rect, canvas: Canvas, p: Paint) { + val savedStyle: Style = p.getStyle() + val stopX = computeDayLeftPosition(mNumDays).toFloat() + var y = 0f + val deltaY = (mCellHeight + HOUR_GAP).toFloat() + var linesIndex = 0 + val startY = 0f + val stopY = (HOUR_GAP + 24 * (mCellHeight + HOUR_GAP)).toFloat() + var x = mHoursWidth.toFloat() + + // Draw the inner horizontal grid lines + p.setColor(mCalendarGridLineInnerHorizontalColor) + p.setStrokeWidth(GRID_LINE_INNER_WIDTH) + p.setAntiAlias(false) + y = 0f + linesIndex = 0 + for (hour in 0..24) { + mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN + mLines[linesIndex++] = y + mLines[linesIndex++] = stopX + mLines[linesIndex++] = y + y += deltaY + } + if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) { + canvas.drawLines(mLines, 0, linesIndex, p) + linesIndex = 0 + p.setColor(mCalendarGridLineInnerVerticalColor) + } + + // Draw the inner vertical grid lines + for (day in 0..mNumDays) { + x = computeDayLeftPosition(day).toFloat() + mLines[linesIndex++] = x + mLines[linesIndex++] = startY + mLines[linesIndex++] = x + mLines[linesIndex++] = stopY + } + canvas.drawLines(mLines, 0, linesIndex, p) + + // Restore the saved style. + p.setStyle(savedStyle) + p.setAntiAlias(true) + } + + /** + * @param r + * @param canvas + * @param p + */ + private fun drawBgColors(r: Rect, canvas: Canvas, p: Paint) { + val todayIndex = mTodayJulianDay - mFirstJulianDay + // Draw the hours background color + r.top = mDestRect.top + r.bottom = mDestRect.bottom + r.left = 0 + r.right = mHoursWidth + p.setColor(mBgColor) + p.setStyle(Style.FILL) + p.setAntiAlias(false) + canvas.drawRect(r, p) + + // Draw background for grid area + if (mNumDays == 1 && todayIndex == 0) { + // Draw a white background for the time later than current time + var lineY: Int = + mCurrentTime!!.hour * (mCellHeight + HOUR_GAP) + mCurrentTime!!.minute * + mCellHeight / 60 + 1 + if (lineY < mViewStartY + mViewHeight) { + lineY = Math.max(lineY, mViewStartY) + r.left = mHoursWidth + r.right = mViewWidth + r.top = lineY + r.bottom = mViewStartY + mViewHeight + p.setColor(mFutureBgColor) + canvas.drawRect(r, p) + } + } else if (todayIndex >= 0 && todayIndex < mNumDays) { + // Draw today with a white background for the time later than current time + var lineY: Int = + mCurrentTime!!.hour * (mCellHeight + HOUR_GAP) + mCurrentTime!!.minute * + mCellHeight / 60 + 1 + if (lineY < mViewStartY + mViewHeight) { + lineY = Math.max(lineY, mViewStartY) + r.left = computeDayLeftPosition(todayIndex) + 1 + r.right = computeDayLeftPosition(todayIndex + 1) + r.top = lineY + r.bottom = mViewStartY + mViewHeight + p.setColor(mFutureBgColor) + canvas.drawRect(r, p) + } + + // Paint Tomorrow and later days with future color + if (todayIndex + 1 < mNumDays) { + r.left = computeDayLeftPosition(todayIndex + 1) + 1 + r.right = computeDayLeftPosition(mNumDays) + r.top = mDestRect.top + r.bottom = mDestRect.bottom + p.setColor(mFutureBgColor) + canvas.drawRect(r, p) + } + } else if (todayIndex < 0) { + // Future + r.left = computeDayLeftPosition(0) + 1 + r.right = computeDayLeftPosition(mNumDays) + r.top = mDestRect.top + r.bottom = mDestRect.bottom + p.setColor(mFutureBgColor) + canvas.drawRect(r, p) + } + p.setAntiAlias(true) + } + + private fun computeMaxStringWidth(currentMax: Int, strings: Array<String?>, p: Paint): Int { + var maxWidthF = 0.0f + val len = strings.size + for (i in 0 until len) { + val width: Float = p.measureText(strings[i]) + maxWidthF = Math.max(width, maxWidthF) + } + var maxWidth = (maxWidthF + 0.5).toInt() + if (maxWidth < currentMax) { + maxWidth = currentMax + } + return maxWidth + } + + private fun saveSelectionPosition(left: Float, top: Float, right: Float, bottom: Float) { + mPrevBox.left = left.toInt() + mPrevBox.right = right.toInt() + mPrevBox.top = top.toInt() + mPrevBox.bottom = bottom.toInt() + } + + private fun setupTextRect(r: Rect) { + if (r.bottom <= r.top || r.right <= r.left) { + r.bottom = r.top + r.right = r.left + return + } + if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) { + r.top += EVENT_TEXT_TOP_MARGIN + r.bottom -= EVENT_TEXT_BOTTOM_MARGIN + } + if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) { + r.left += EVENT_TEXT_LEFT_MARGIN + r.right -= EVENT_TEXT_RIGHT_MARGIN + } + } + + private fun setupAllDayTextRect(r: Rect) { + if (r.bottom <= r.top || r.right <= r.left) { + r.bottom = r.top + r.right = r.left + return + } + if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) { + r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN + r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN + } + if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) { + r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN + r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN + } + } + + /** + * Return the layout for a numbered event. Create it if not already existing + */ + private fun getEventLayout( + layouts: Array<StaticLayout?>?, + i: Int, + event: Event, + paint: Paint, + r: Rect + ): StaticLayout? { + if (i < 0 || i >= layouts!!.size) { + return null + } + var layout: StaticLayout? = layouts!![i] + // Check if we have already initialized the StaticLayout and that + // the width hasn't changed (due to vertical resizing which causes + // re-layout of events at min height) + if (layout == null || r.width() !== layout.getWidth()) { + val bob = SpannableStringBuilder() + if (event.title != null) { + // MAX - 1 since we add a space + bob.append(drawTextSanitizer(event.title.toString(), + MAX_EVENT_TEXT_LEN - 1)) + bob.setSpan(StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length, 0) + bob.append(' ') + } + if (event.location != null) { + bob.append( + drawTextSanitizer( + event.location.toString(), + MAX_EVENT_TEXT_LEN - bob.length + ) + ) + } + when (event.selfAttendeeStatus) { + Attendees.ATTENDEE_STATUS_INVITED -> paint.setColor(event.color) + Attendees.ATTENDEE_STATUS_DECLINED -> { + paint.setColor(mEventTextColor) + paint.setAlpha(Utils.DECLINED_EVENT_TEXT_ALPHA) + } + Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED, + Attendees.ATTENDEE_STATUS_TENTATIVE -> paint.setColor( + mEventTextColor + ) + else -> paint.setColor(mEventTextColor) + } + + // Leave a one pixel boundary on the left and right of the rectangle for the event + layout = StaticLayout( + bob, 0, bob.length, TextPaint(paint), r.width(), + Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width() + ) + layouts[i] = layout + } + layout.getPaint().setAlpha(mEventsAlpha) + return layout + } + + private fun drawAllDayEvents(firstDay: Int, numDays: Int, canvas: Canvas, p: Paint) { + p.setTextSize(NORMAL_FONT_SIZE) + p.setTextAlign(Paint.Align.LEFT) + val eventTextPaint: Paint = mEventTextPaint + val startY = DAY_HEADER_HEIGHT.toFloat() + val stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN + var x = 0f + var linesIndex = 0 + + // Draw the inner vertical grid lines + p.setColor(mCalendarGridLineInnerVerticalColor) + x = mHoursWidth.toFloat() + p.setStrokeWidth(GRID_LINE_INNER_WIDTH) + // Line bounding the top of the all day area + mLines!![linesIndex++] = GRID_LINE_LEFT_MARGIN + mLines!![linesIndex++] = startY + mLines!![linesIndex++] = computeDayLeftPosition(mNumDays).toFloat() + mLines!![linesIndex++] = startY + for (day in 0..mNumDays) { + x = computeDayLeftPosition(day).toFloat() + mLines!![linesIndex++] = x + mLines!![linesIndex++] = startY + mLines!![linesIndex++] = x + mLines!![linesIndex++] = stopY + } + p.setAntiAlias(false) + canvas.drawLines(mLines, 0, linesIndex, p) + p.setStyle(Style.FILL) + val y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN + val lastDay = firstDay + numDays - 1 + val events: ArrayList<Event>? = mAllDayEvents + val numEvents: Int = events!!.size + // Whether or not we should draw the more events text + var hasMoreEvents = false + // size of the allDay area + val drawHeight = mAlldayHeight.toFloat() + // max number of events being drawn in one day of the allday area + var numRectangles = mMaxAlldayEvents.toFloat() + // Where to cut off drawn allday events + var allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN + // The number of events that weren't drawn in each day + mSkippedAlldayEvents = IntArray(numDays) + if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && + !mShowAllAllDayEvents && mAnimateDayHeight == 0) { + // We draw one fewer event than will fit so that more events text + // can be drawn + numRectangles = (mMaxUnexpandedAlldayEventCount - 1).toFloat() + // We also clip the events above the more events text + allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt() + hasMoreEvents = true + } else if (mAnimateDayHeight != 0) { + // clip at the end of the animating space + allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN + } + var alpha: Int = eventTextPaint.getAlpha() + eventTextPaint.setAlpha(mEventsAlpha) + for (i in 0 until numEvents) { + val event: Event = events!!.get(i) + var startDay: Int = event.startDay + var endDay: Int = event.endDay + if (startDay > lastDay || endDay < firstDay) { + continue + } + if (startDay < firstDay) { + startDay = firstDay + } + if (endDay > lastDay) { + endDay = lastDay + } + val startIndex = startDay - firstDay + val endIndex = endDay - firstDay + var height = + if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) + mAnimateDayEventHeight.toFloat() else drawHeight / numRectangles + + // Prevent a single event from getting too big + if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { + height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT.toFloat() + } + + // Leave a one-pixel space between the vertical day lines and the + // event rectangle. + event.left = computeDayLeftPosition(startIndex).toFloat() + event.right = computeDayLeftPosition(endIndex + 1).toFloat() - DAY_GAP + event.top = y + height * event.getColumn() + event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN + if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { + // check if we should skip this event. We skip if it starts + // after the clip bound or ends after the skip bound and we're + // not animating. + if (event.top >= allDayEventClip) { + incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex) + continue + } else if (event.bottom > allDayEventClip) { + if (hasMoreEvents) { + incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex) + continue + } + event.bottom = allDayEventClip.toFloat() + } + } + val r: Rect = drawEventRect( + event, canvas, p, eventTextPaint, event.top.toInt(), + event.bottom.toInt() + ) + setupAllDayTextRect(r) + val layout: StaticLayout? = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r) + drawEventText(layout, r, canvas, r.top, r.bottom, true) + + // Check if this all-day event intersects the selected day + if (mSelectionAllday && mComputeSelectedEvents) { + if (startDay <= mSelectionDay && endDay >= mSelectionDay) { + mSelectedEvents.add(event) + } + } + } + eventTextPaint.setAlpha(alpha) + if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) { + // If the more allday text should be visible, draw it. + alpha = p.getAlpha() + p.setAlpha(mEventsAlpha) + p.setColor(mMoreAlldayEventsTextAlpha shl 24 and mMoreEventsTextColor) + for (i in mSkippedAlldayEvents!!.indices) { + if (mSkippedAlldayEvents!![i] > 0) { + drawMoreAlldayEvents(canvas, mSkippedAlldayEvents!![i], i, p) + } + } + p.setAlpha(alpha) + } + if (mSelectionAllday) { + // Compute the neighbors for the list of all-day events that + // intersect the selected day. + computeAllDayNeighbors() + + // Set the selection position to zero so that when we move down + // to the normal event area, we will highlight the topmost event. + saveSelectionPosition(0f, 0f, 0f, 0f) + } + } + + // Helper method for counting the number of allday events skipped on each day + private fun incrementSkipCount(counts: IntArray?, startIndex: Int, endIndex: Int) { + if (counts == null || startIndex < 0 || endIndex > counts.size) { + return + } + for (i in startIndex..endIndex) { + counts[i]++ + } + } + + // Draws the "box +n" text for hidden allday events + protected fun drawMoreAlldayEvents(canvas: Canvas, remainingEvents: Int, day: Int, p: Paint) { + var x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN + var y = (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - (.5f * + EVENT_SQUARE_WIDTH) + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN).toInt() + val r: Rect = mRect + r.top = y + r.left = x + r.bottom = y + EVENT_SQUARE_WIDTH + r.right = x + EVENT_SQUARE_WIDTH + p.setColor(mMoreEventsTextColor) + p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH.toFloat()) + p.setStyle(Style.STROKE) + p.setAntiAlias(false) + canvas.drawRect(r, p) + p.setAntiAlias(true) + p.setStyle(Style.FILL) + p.setTextSize(EVENT_TEXT_FONT_SIZE) + val text: String = + mResources.getQuantityString(R.plurals.month_more_events, remainingEvents) + y += EVENT_SQUARE_WIDTH + x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING + canvas.drawText(String.format(text, remainingEvents), x.toFloat(), y.toFloat(), p) + } + + private fun computeAllDayNeighbors() { + val len: Int = mSelectedEvents.size + if (len == 0 || mSelectedEvent != null) { + return + } + + // First, clear all the links + for (ii in 0 until len) { + val ev: Event = mSelectedEvents.get(ii) + ev.nextUp = null + ev.nextDown = null + ev.nextLeft = null + ev.nextRight = null + } + + // For each event in the selected event list "mSelectedEvents", find + // its neighbors in the up and down directions. This could be done + // more efficiently by sorting on the Event.getColumn() field, but + // the list is expected to be very small. + + // Find the event in the same row as the previously selected all-day + // event, if any. + var startPosition = -1 + if (mPrevSelectedEvent != null && mPrevSelectedEvent!!.drawAsAllday()) { + startPosition = mPrevSelectedEvent?.getColumn() as Int + } + var maxPosition = -1 + var startEvent: Event? = null + var maxPositionEvent: Event? = null + for (ii in 0 until len) { + val ev: Event = mSelectedEvents.get(ii) + val position: Int = ev.getColumn() + if (position == startPosition) { + startEvent = ev + } else if (position > maxPosition) { + maxPositionEvent = ev + maxPosition = position + } + for (jj in 0 until len) { + if (jj == ii) { + continue + } + val neighbor: Event = mSelectedEvents.get(jj) + val neighborPosition: Int = neighbor.getColumn() + if (neighborPosition == position - 1) { + ev.nextUp = neighbor + } else if (neighborPosition == position + 1) { + ev.nextDown = neighbor + } + } + } + if (startEvent != null) { + setSelectedEvent(startEvent) + } else { + setSelectedEvent(maxPositionEvent) + } + } + + private fun drawEvents(date: Int, dayIndex: Int, top: Int, canvas: Canvas, p: Paint) { + val eventTextPaint: Paint = mEventTextPaint + val left = computeDayLeftPosition(dayIndex) + 1 + val cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1 + val cellHeight = mCellHeight + + // Use the selected hour as the selection region + val selectionArea: Rect = mSelectionRect + selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP) + selectionArea.bottom = selectionArea.top + cellHeight + selectionArea.left = left + selectionArea.right = selectionArea.left + cellWidth + val events: ArrayList<Event> = mEvents + val numEvents: Int = events.size + val geometry: EventGeometry = mEventGeometry + val viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight + val alpha: Int = eventTextPaint.getAlpha() + eventTextPaint.setAlpha(mEventsAlpha) + for (i in 0 until numEvents) { + val event: Event = events.get(i) + if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { + continue + } + + // Don't draw it if it is not visible + if (event.bottom < mViewStartY || event.top > viewEndY) { + continue + } + if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents && + geometry.eventIntersectsSelection(event, selectionArea) + ) { + mSelectedEvents.add(event) + } + val r: Rect = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY) + setupTextRect(r) + + // Don't draw text if it is not visible + if (r.top > viewEndY || r.bottom < mViewStartY) { + continue + } + val layout: StaticLayout? = getEventLayout(mLayouts, i, event, eventTextPaint, r) + // TODO: not sure why we are 4 pixels off + drawEventText( + layout, + r, + canvas, + mViewStartY + 4, + mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight, + false + ) + } + eventTextPaint.setAlpha(alpha) + } + + private fun drawEventRect( + event: Event, + canvas: Canvas, + p: Paint, + eventTextPaint: Paint, + visibleTop: Int, + visibleBot: Int + ): Rect { + // Draw the Event Rect + val r: Rect = mRect + r.top = Math.max(event.top.toInt() + EVENT_RECT_TOP_MARGIN, visibleTop) + r.bottom = Math.min(event.bottom.toInt() - EVENT_RECT_BOTTOM_MARGIN, visibleBot) + r.left = event.left.toInt() + EVENT_RECT_LEFT_MARGIN + r.right = event.right.toInt() + var color: Int = event.color + when (event.selfAttendeeStatus) { + Attendees.ATTENDEE_STATUS_INVITED -> if (event !== mClickedEvent) { + p.setStyle(Style.STROKE) + } + Attendees.ATTENDEE_STATUS_DECLINED -> { + if (event !== mClickedEvent) { + color = Utils.getDeclinedColorFromColor(color) + } + p.setStyle(Style.FILL_AND_STROKE) + } + Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED, + Attendees.ATTENDEE_STATUS_TENTATIVE -> p.setStyle( + Style.FILL_AND_STROKE + ) + else -> p.setStyle(Style.FILL_AND_STROKE) + } + p.setAntiAlias(false) + val floorHalfStroke = Math.floor(EVENT_RECT_STROKE_WIDTH.toDouble() / 2.0).toInt() + val ceilHalfStroke = Math.ceil(EVENT_RECT_STROKE_WIDTH.toDouble() / 2.0).toInt() + r.top = Math.max(event.top.toInt() + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop) + r.bottom = Math.min( + event.bottom.toInt() - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke, + visibleBot + ) + r.left += floorHalfStroke + r.right -= ceilHalfStroke + p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH.toFloat()) + p.setColor(color) + val alpha: Int = p.getAlpha() + p.setAlpha(mEventsAlpha) + canvas.drawRect(r, p) + p.setAlpha(alpha) + p.setStyle(Style.FILL) + + // Setup rect for drawEventText which follows + r.top = event.top.toInt() + EVENT_RECT_TOP_MARGIN + r.bottom = event.bottom.toInt() - EVENT_RECT_BOTTOM_MARGIN + r.left = event.left.toInt() + EVENT_RECT_LEFT_MARGIN + r.right = event.right.toInt() - EVENT_RECT_RIGHT_MARGIN + return r + } + + private val drawTextSanitizerFilter: Pattern = Pattern.compile("[\t\n],") + + // Sanitize a string before passing it to drawText or else we get little + // squares. For newlines and tabs before a comma, delete the character. + // Otherwise, just replace them with a space. + private fun drawTextSanitizer(string: String, maxEventTextLen: Int): String { + var string = string + val m: Matcher = drawTextSanitizerFilter.matcher(string) + string = m.replaceAll(",") + var len: Int = string.length + if (maxEventTextLen <= 0) { + string = "" + len = 0 + } else if (len > maxEventTextLen) { + string = string.substring(0, maxEventTextLen) + len = maxEventTextLen + } + return string.replace('\n', ' ') + } + + private fun drawEventText( + eventLayout: StaticLayout?, + rect: Rect, + canvas: Canvas, + top: Int, + bottom: Int, + center: Boolean + ) { + // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging + val width: Int = rect.right - rect.left + val height: Int = rect.bottom - rect.top + + // If the rectangle is too small for text, then return + if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) { + return + } + var totalLineHeight = 0 + val lineCount: Int = eventLayout.getLineCount() + for (i in 0 until lineCount) { + val lineBottom: Int = eventLayout.getLineBottom(i) + totalLineHeight = if (lineBottom <= height) { + lineBottom + } else { + break + } + } + + // + 2 is small workaround when the font is slightly bigger than the rect. This will + // still allow the text to be shown without overflowing into the other all day rects. + if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight + 2 < top) { + return + } + + // Use a StaticLayout to format the string. + canvas.save() + // canvas.translate(rect.left, rect.top + (rect.bottom - rect.top / 2)); + val padding = if (center) (rect.bottom - rect.top - totalLineHeight) / 2 else 0 + canvas.translate(rect.left.toFloat(), rect.top.toFloat() + padding) + rect.left = 0 + rect.right = width + rect.top = 0 + rect.bottom = totalLineHeight + + // There's a bug somewhere. If this rect is outside of a previous + // cliprect, this becomes a no-op. What happens is that the text draw + // past the event rect. The current fix is to not draw the staticLayout + // at all if it is completely out of bound. + canvas.clipRect(rect) + eventLayout.draw(canvas) + canvas.restore() + } + + // The following routines are called from the parent activity when certain + // touch events occur. + private fun doDown(ev: MotionEvent) { + mTouchMode = TOUCH_MODE_DOWN + mViewStartX = 0 + mOnFlingCalled = false + mHandler?.removeCallbacks(mContinueScroll) + val x = ev.getX().toInt() + val y = ev.getY().toInt() + + // Save selection information: we use setSelectionFromPosition to find the selected event + // in order to show the "clicked" color. But since it is also setting the selected info + // for new events, we need to restore the old info after calling the function. + val oldSelectedEvent: Event? = mSelectedEvent + val oldSelectionDay = mSelectionDay + val oldSelectionHour = mSelectionHour + if (setSelectionFromPosition(x, y, false)) { + // If a time was selected (a blue selection box is visible) and the click location + // is in the selected time, do not show a click on an event to prevent a situation + // of both a selection and an event are clicked when they overlap. + val pressedSelected = (mSelectionMode != SELECTION_HIDDEN && + oldSelectionDay == mSelectionDay && oldSelectionHour == mSelectionHour) + if (!pressedSelected && mSelectedEvent != null) { + mSavedClickedEvent = mSelectedEvent + mDownTouchTime = System.currentTimeMillis() + postDelayed(mSetClick, mOnDownDelay.toLong()) + } else { + eventClickCleanup() + } + } + mSelectedEvent = oldSelectedEvent + mSelectionDay = oldSelectionDay + mSelectionHour = oldSelectionHour + invalidate() + } + + // Kicks off all the animations when the expand allday area is tapped + private fun doExpandAllDayClick() { + mShowAllAllDayEvents = !mShowAllAllDayEvents + ObjectAnimator.setFrameDelay(0) + + // Determine the starting height + if (mAnimateDayHeight == 0) { + mAnimateDayHeight = + if (mShowAllAllDayEvents) mAlldayHeight - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt() + else mAlldayHeight + } + // Cancel current animations + mCancellingAnimations = true + if (mAlldayAnimator != null) { + mAlldayAnimator?.cancel() + } + if (mAlldayEventAnimator != null) { + mAlldayEventAnimator?.cancel() + } + if (mMoreAlldayEventsAnimator != null) { + mMoreAlldayEventsAnimator?.cancel() + } + mCancellingAnimations = false + // get new animators + mAlldayAnimator = allDayAnimator + mAlldayEventAnimator = allDayEventAnimator + mMoreAlldayEventsAnimator = ObjectAnimator.ofInt( + this, + "moreAllDayEventsTextAlpha", + if (mShowAllAllDayEvents) MORE_EVENTS_MAX_ALPHA else 0, + if (mShowAllAllDayEvents) 0 else MORE_EVENTS_MAX_ALPHA + ) + + // Set up delays and start the animators + mAlldayAnimator?.setStartDelay(if (mShowAllAllDayEvents) ANIMATION_SECONDARY_DURATION + else 0) + mAlldayAnimator?.start() + mMoreAlldayEventsAnimator?.setStartDelay(if (mShowAllAllDayEvents) 0 + else ANIMATION_DURATION) + mMoreAlldayEventsAnimator?.setDuration(ANIMATION_SECONDARY_DURATION) + mMoreAlldayEventsAnimator?.start() + if (mAlldayEventAnimator != null) { + // This is the only animator that can return null, so check it + mAlldayEventAnimator + ?.setStartDelay(if (mShowAllAllDayEvents) ANIMATION_SECONDARY_DURATION else 0) + mAlldayEventAnimator?.start() + } + } + + /** + * Figures out the initial heights for allDay events and space when + * a view is being set up. + */ + fun initAllDayHeights() { + if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) { + return + } + if (mShowAllAllDayEvents) { + var maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT + maxADHeight = Math.min( + maxADHeight, + (mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt() + ) + mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents + } else { + mAnimateDayEventHeight = MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt() + } + } // First calculate the absolute max height + // Now expand to fit but not beyond the absolute max + // calculate the height of individual events in order to fit + // if there's nothing to animate just return + + // Set up the animator with the calculated values + // Sets up an animator for changing the height of allday events + private val allDayEventAnimator: ObjectAnimator? + private get() { + // First calculate the absolute max height + var maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT + // Now expand to fit but not beyond the absolute max + maxADHeight = Math.min( + maxADHeight, + (mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt() + ) + // calculate the height of individual events in order to fit + val fitHeight = maxADHeight / mMaxAlldayEvents + val currentHeight = mAnimateDayEventHeight + val desiredHeight = + if (mShowAllAllDayEvents) fitHeight else MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt() + // if there's nothing to animate just return + if (currentHeight == desiredHeight) { + return null + } + + // Set up the animator with the calculated values + val animator: ObjectAnimator = ObjectAnimator.ofInt( + this, "animateDayEventHeight", + currentHeight, desiredHeight + ) + animator.setDuration(ANIMATION_DURATION) + return animator + } + + // Set up the animator with the calculated values + // Sets up an animator for changing the height of the allday area + private val allDayAnimator: ObjectAnimator + private get() { + // Calculate the absolute max height + var maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT + // Find the desired height but don't exceed abs max + maxADHeight = Math.min( + maxADHeight, + (mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT).toInt() + ) + // calculate the current and desired heights + val currentHeight = if (mAnimateDayHeight != 0) mAnimateDayHeight else mAlldayHeight + val desiredHeight = + if (mShowAllAllDayEvents) maxADHeight else (MAX_UNEXPANDED_ALLDAY_HEIGHT - + MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1).toInt() + + // Set up the animator with the calculated values + val animator: ObjectAnimator = ObjectAnimator.ofInt( + this, "animateDayHeight", + currentHeight, desiredHeight + ) + animator.setDuration(ANIMATION_DURATION) + animator.addListener(object : AnimatorListenerAdapter() { + @Override + override fun onAnimationEnd(animation: Animator) { + if (!mCancellingAnimations) { + // when finished, set this to 0 to signify not animating + mAnimateDayHeight = 0 + mUseExpandIcon = !mShowAllAllDayEvents + } + mRemeasure = true + invalidate() + } + }) + return animator + } + + // setter for the 'box +n' alpha text used by the animator + fun setMoreAllDayEventsTextAlpha(alpha: Int) { + mMoreAlldayEventsTextAlpha = alpha + invalidate() + } + + // setter for the height of the allday area used by the animator + fun setAnimateDayHeight(height: Int) { + mAnimateDayHeight = height + mRemeasure = true + invalidate() + } + + // setter for the height of allday events used by the animator + fun setAnimateDayEventHeight(height: Int) { + mAnimateDayEventHeight = height + mRemeasure = true + invalidate() + } + + private fun doSingleTapUp(ev: MotionEvent) { + if (!mHandleActionUp || mScrolling) { + return + } + val x = ev.getX().toInt() + val y = ev.getY().toInt() + val selectedDay = mSelectionDay + val selectedHour = mSelectionHour + if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { + // check if the tap was in the allday expansion area + val bottom = mFirstCell + if (x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight || + !mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom && y >= bottom - + MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT + ) { + doExpandAllDayClick() + return + } + } + val validPosition = setSelectionFromPosition(x, y, false) + if (!validPosition) { + if (y < DAY_HEADER_HEIGHT) { + val selectedTime = Time(mBaseDate) + selectedTime.setJulianDay(mSelectionDay) + selectedTime.hour = mSelectionHour + selectedTime.normalize(true /* ignore isDst */) + mController.sendEvent( + this as? Object, EventType.GO_TO, null, null, selectedTime, -1, + ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null + ) + } + return + } + val hasSelection = mSelectionMode != SELECTION_HIDDEN + val pressedSelected = ((hasSelection || mTouchExplorationEnabled) && + selectedDay == mSelectionDay && selectedHour == mSelectionHour) + if (mSelectedEvent != null) { + // If the tap is on an event, launch the "View event" view + if (mIsAccessibilityEnabled) { + mAccessibilityMgr?.interrupt() + } + mSelectionMode = SELECTION_HIDDEN + var yLocation = ((mSelectedEvent!!.top + mSelectedEvent!!.bottom) / 2) as Int + // Y location is affected by the position of the event in the scrolling + // view (mViewStartY) and the presence of all day events (mFirstCell) + if (!mSelectedEvent!!.allDay) { + yLocation += mFirstCell - mViewStartY + } + mClickedYLocation = yLocation + val clearDelay: Long = CLICK_DISPLAY_DURATION + mOnDownDelay - + (System.currentTimeMillis() - mDownTouchTime) + if (clearDelay > 0) { + this.postDelayed(mClearClick, clearDelay) + } else { + this.post(mClearClick) + } + } + invalidate() + } + + private fun doLongPress(ev: MotionEvent) { + eventClickCleanup() + if (mScrolling) { + return + } + + // Scale gesture in progress + if (mStartingSpanY != 0f) { + return + } + val x = ev.getX().toInt() + val y = ev.getY().toInt() + val validPosition = setSelectionFromPosition(x, y, false) + if (!validPosition) { + // return if the touch wasn't on an area of concern + return + } + invalidate() + performLongClick() + } + + private fun doScroll(e1: MotionEvent, e2: MotionEvent, deltaX: Float, deltaY: Float) { + cancelAnimation() + if (mStartingScroll) { + mInitialScrollX = 0f + mInitialScrollY = 0f + mStartingScroll = false + } + mInitialScrollX += deltaX + mInitialScrollY += deltaY + val distanceX = mInitialScrollX.toInt() + val distanceY = mInitialScrollY.toInt() + val focusY = getAverageY(e2) + if (mRecalCenterHour) { + // Calculate the hour that correspond to the average of the Y touch points + mGestureCenterHour = ((mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) / + (mCellHeight + DAY_GAP)) + mRecalCenterHour = false + } + + // If we haven't figured out the predominant scroll direction yet, + // then do it now. + if (mTouchMode == TOUCH_MODE_DOWN) { + val absDistanceX: Int = Math.abs(distanceX) + val absDistanceY: Int = Math.abs(distanceY) + mScrollStartY = mViewStartY + mPreviousDirection = 0 + if (absDistanceX > absDistanceY) { + val slopFactor = if (mScaleGestureDetector.isInProgress()) 20 else 2 + if (absDistanceX > mScaledPagingTouchSlop * slopFactor) { + mTouchMode = TOUCH_MODE_HSCROLL + mViewStartX = distanceX + initNextView(-mViewStartX) + } + } else { + mTouchMode = TOUCH_MODE_VSCROLL + } + } else if (mTouchMode and TOUCH_MODE_HSCROLL != 0) { + // We are already scrolling horizontally, so check if we + // changed the direction of scrolling so that the other week + // is now visible. + mViewStartX = distanceX + if (distanceX != 0) { + val direction = if (distanceX > 0) 1 else -1 + if (direction != mPreviousDirection) { + // The user has switched the direction of scrolling + // so re-init the next view + initNextView(-mViewStartX) + mPreviousDirection = direction + } + } + } + if (mTouchMode and TOUCH_MODE_VSCROLL != 0) { + // Calculate the top of the visible region in the calendar grid. + // Increasing/decrease this will scroll the calendar grid up/down. + mViewStartY = ((mGestureCenterHour * (mCellHeight + DAY_GAP) - + focusY) + DAY_HEADER_HEIGHT + mAlldayHeight).toInt() + + // If dragging while already at the end, do a glow + val pulledToY = (mScrollStartY + deltaY).toInt() + if (pulledToY < 0) { + mEdgeEffectTop.onPull(deltaY / mViewHeight) + if (!mEdgeEffectBottom.isFinished()) { + mEdgeEffectBottom.onRelease() + } + } else if (pulledToY > mMaxViewStartY) { + mEdgeEffectBottom.onPull(deltaY / mViewHeight) + if (!mEdgeEffectTop.isFinished()) { + mEdgeEffectTop.onRelease() + } + } + if (mViewStartY < 0) { + mViewStartY = 0 + mRecalCenterHour = true + } else if (mViewStartY > mMaxViewStartY) { + mViewStartY = mMaxViewStartY + mRecalCenterHour = true + } + if (mRecalCenterHour) { + // Calculate the hour that correspond to the average of the Y touch points + mGestureCenterHour = ((mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) / + (mCellHeight + DAY_GAP)) + mRecalCenterHour = false + } + computeFirstHour() + } + mScrolling = true + mSelectionMode = SELECTION_HIDDEN + invalidate() + } + + private fun getAverageY(me: MotionEvent): Float { + val count: Int = me.getPointerCount() + var focusY = 0f + for (i in 0 until count) { + focusY += me.getY(i) + } + focusY /= count.toFloat() + return focusY + } + + private fun cancelAnimation() { + val `in`: Animation? = mViewSwitcher?.getInAnimation() + if (`in` != null) { + // cancel() doesn't terminate cleanly. + `in`?.scaleCurrentDuration(0f) + } + val out: Animation? = mViewSwitcher?.getOutAnimation() + if (out != null) { + // cancel() doesn't terminate cleanly. + out?.scaleCurrentDuration(0f) + } + } + + private fun doFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float) { + cancelAnimation() + mSelectionMode = SELECTION_HIDDEN + eventClickCleanup() + mOnFlingCalled = true + if (mTouchMode and TOUCH_MODE_HSCROLL != 0) { + // Horizontal fling. + // initNextView(deltaX); + mTouchMode = TOUCH_MODE_INITIAL_STATE + if (DEBUG) Log.d(TAG, "doFling: velocityX $velocityX") + val deltaX = e2.getX().toInt() - e1.getX().toInt() + switchViews(deltaX < 0, mViewStartX.toFloat(), mViewWidth.toFloat(), velocityX) + mViewStartX = 0 + return + } + if (mTouchMode and TOUCH_MODE_VSCROLL == 0) { + if (DEBUG) Log.d(TAG, "doFling: no fling") + return + } + + // Vertical fling. + mTouchMode = TOUCH_MODE_INITIAL_STATE + mViewStartX = 0 + if (DEBUG) { + Log.d(TAG, "doFling: mViewStartY$mViewStartY velocityY $velocityY") + } + + // Continue scrolling vertically + mScrolling = true + mScroller.fling( + 0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */, + (-velocityY).toInt(), 0 /* minX */, 0 /* maxX */, 0 /* minY */, + mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE + ) + + // When flinging down, show a glow when it hits the end only if it + // wasn't started at the top + if (velocityY > 0 && mViewStartY != 0) { + mCallEdgeEffectOnAbsorb = true + } else if (velocityY < 0 && mViewStartY != mMaxViewStartY) { + mCallEdgeEffectOnAbsorb = true + } + mHandler?.post(mContinueScroll) + } + + private fun initNextView(deltaX: Int): Boolean { + // Change the view to the previous day or week + val view = mViewSwitcher.getNextView() as DayView + val date: Time? = view.mBaseDate + date?.set(mBaseDate) + val switchForward: Boolean + if (deltaX > 0) { + date!!.monthDay -= mNumDays + view.setSelectedDay(mSelectionDay - mNumDays) + switchForward = false + } else { + date!!.monthDay += mNumDays + view.setSelectedDay(mSelectionDay + mNumDays) + switchForward = true + } + date?.normalize(true /* ignore isDst */) + initView(view) + view.layout(getLeft(), getTop(), getRight(), getBottom()) + view.reloadEvents() + return switchForward + } + + // ScaleGestureDetector.OnScaleGestureListener + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + mHandleActionUp = false + val gestureCenterInPixels: Float = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight + mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP) + mStartingSpanY = Math.max(MIN_Y_SPAN.toFloat(), + Math.abs(detector.getCurrentSpanY().toFloat())) + mCellHeightBeforeScaleGesture = mCellHeight + if (DEBUG_SCALING) { + val ViewStartHour = mViewStartY / (mCellHeight + DAY_GAP).toFloat() + Log.d( + TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour + + "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY + + "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY() + ) + } + return true + } + + // ScaleGestureDetector.OnScaleGestureListener + override fun onScale(detector: ScaleGestureDetector): Boolean { + val spanY: Float = Math.max(MIN_Y_SPAN.toFloat(), + Math.abs(detector.getCurrentSpanY().toFloat())) + mCellHeight = (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY).toInt() + if (mCellHeight < mMinCellHeight) { + // If mStartingSpanY is too small, even a small increase in the + // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT + mStartingSpanY = spanY + mCellHeight = mMinCellHeight + mCellHeightBeforeScaleGesture = mMinCellHeight + } else if (mCellHeight > MAX_CELL_HEIGHT) { + mStartingSpanY = spanY + mCellHeight = MAX_CELL_HEIGHT + mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT + } + val gestureCenterInPixels = detector.getFocusY().toInt() - DAY_HEADER_HEIGHT - mAlldayHeight + mViewStartY = (mGestureCenterHour * (mCellHeight + DAY_GAP)).toInt() - gestureCenterInPixels + mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight + if (DEBUG_SCALING) { + val ViewStartHour = mViewStartY / (mCellHeight + DAY_GAP).toFloat() + Log.d( + TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " + + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" + + mCellHeight + " SpanY:" + detector.getCurrentSpanY() + ) + } + if (mViewStartY < 0) { + mViewStartY = 0 + mGestureCenterHour = ((mViewStartY + gestureCenterInPixels) / + (mCellHeight + DAY_GAP).toFloat()) + } else if (mViewStartY > mMaxViewStartY) { + mViewStartY = mMaxViewStartY + mGestureCenterHour = ((mViewStartY + gestureCenterInPixels) / + (mCellHeight + DAY_GAP).toFloat()) + } + computeFirstHour() + mRemeasure = true + invalidate() + return true + } + + // ScaleGestureDetector.OnScaleGestureListener + override fun onScaleEnd(detector: ScaleGestureDetector) { + mScrollStartY = mViewStartY + mInitialScrollY = 0f + mInitialScrollX = 0f + mStartingSpanY = 0f + } + + @Override + override fun onTouchEvent(ev: MotionEvent): Boolean { + val action: Int = ev.getAction() + if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount()) + if (ev.getActionMasked() === MotionEvent.ACTION_DOWN || + ev.getActionMasked() === MotionEvent.ACTION_UP || + ev.getActionMasked() === MotionEvent.ACTION_POINTER_UP || + ev.getActionMasked() === MotionEvent.ACTION_POINTER_DOWN + ) { + mRecalCenterHour = true + } + if (mTouchMode and TOUCH_MODE_HSCROLL == 0) { + mScaleGestureDetector.onTouchEvent(ev) + } + return when (action) { + MotionEvent.ACTION_DOWN -> { + mStartingScroll = true + if (DEBUG) { + Log.e( + TAG, + "ACTION_DOWN ev.getDownTime = " + ev.getDownTime().toString() + " Cnt=" + + ev.getPointerCount() + ) + } + val bottom = + mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN + mTouchStartedInAlldayArea = if (ev.getY() < bottom) { + true + } else { + false + } + mHandleActionUp = true + mGestureDetector.onTouchEvent(ev) + true + } + MotionEvent.ACTION_MOVE -> { + if (DEBUG) Log.e( + TAG, + "ACTION_MOVE Cnt=" + ev.getPointerCount() + this@DayView + ) + mGestureDetector.onTouchEvent(ev) + true + } + MotionEvent.ACTION_UP -> { + if (DEBUG) Log.e( + TAG, + "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp + ) + mEdgeEffectTop.onRelease() + mEdgeEffectBottom.onRelease() + mStartingScroll = false + mGestureDetector.onTouchEvent(ev) + if (!mHandleActionUp) { + mHandleActionUp = true + mViewStartX = 0 + invalidate() + return true + } + if (mOnFlingCalled) { + return true + } + + // If we were scrolling, then reset the selected hour so that it + // is visible. + if (mScrolling) { + mScrolling = false + resetSelectedHour() + invalidate() + } + if (mTouchMode and TOUCH_MODE_HSCROLL != 0) { + mTouchMode = TOUCH_MODE_INITIAL_STATE + if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) { + // The user has gone beyond the threshold so switch views + if (DEBUG) Log.d( + TAG, + "- horizontal scroll: switch views" + ) + switchViews( + mViewStartX > 0, + mViewStartX.toFloat(), + mViewWidth.toFloat(), + 0f + ) + mViewStartX = 0 + return true + } else { + // Not beyond the threshold so invalidate which will cause + // the view to snap back. Also call recalc() to ensure + // that we have the correct starting date and title. + if (DEBUG) Log.d( + TAG, + "- horizontal scroll: snap back" + ) + recalc() + invalidate() + mViewStartX = 0 + } + } + true + } + MotionEvent.ACTION_CANCEL -> { + if (DEBUG) Log.e( + TAG, + "ACTION_CANCEL" + ) + mGestureDetector.onTouchEvent(ev) + mScrolling = false + resetSelectedHour() + true + } + else -> { + if (DEBUG) Log.e( + TAG, + "Not MotionEvent " + ev.toString() + ) + if (mGestureDetector.onTouchEvent(ev)) { + true + } else super.onTouchEvent(ev) + } + } + } + + override fun onCreateContextMenu(menu: ContextMenu, view: View?, menuInfo: ContextMenuInfo?) { + var item: MenuItem + + // If the trackball is held down, then the context menu pops up and + // we never get onKeyUp() for the long-press. So check for it here + // and change the selection to the long-press state. + if (mSelectionMode != SELECTION_LONGPRESS) { + invalidate() + } + val startMillis = selectedTimeInMillis + val flags: Int = (DateUtils.FORMAT_SHOW_TIME + or DateUtils.FORMAT_CAP_NOON_MIDNIGHT + or DateUtils.FORMAT_SHOW_WEEKDAY) + val title: String? = Utils.formatDateRange(mContext, startMillis, startMillis, flags) + menu.setHeaderTitle(title) + mPopup?.dismiss() + } + + /** + * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position. + * If the touch position is not within the displayed grid, then this + * method returns false. + * + * @param x the x position of the touch + * @param y the y position of the touch + * @param keepOldSelection - do not change the selection info (used for invoking accessibility + * messages) + * @return true if the touch position is valid + */ + private fun setSelectionFromPosition(x: Int, y: Int, keepOldSelection: Boolean): Boolean { + var x = x + var savedEvent: Event? = null + var savedDay = 0 + var savedHour = 0 + var savedAllDay = false + if (keepOldSelection) { + // Store selection info and restore it at the end. This way, we can invoke the + // right accessibility message without affecting the selection. + savedEvent = mSelectedEvent + savedDay = mSelectionDay + savedHour = mSelectionHour + savedAllDay = mSelectionAllday + } + if (x < mHoursWidth) { + x = mHoursWidth + } + var day = (x - mHoursWidth) / (mCellWidth + DAY_GAP) + if (day >= mNumDays) { + day = mNumDays - 1 + } + day += mFirstJulianDay + setSelectedDay(day) + if (y < DAY_HEADER_HEIGHT) { + sendAccessibilityEventAsNeeded(false) + return false + } + setSelectedHour(mFirstHour) /* First fully visible hour */ + mSelectionAllday = if (y < mFirstCell) { + true + } else { + // y is now offset from top of the scrollable region + val adjustedY = y - mFirstCell + if (adjustedY < mFirstHourOffset) { + setSelectedHour(mSelectionHour - 1) /* In the partially visible hour */ + } else { + setSelectedHour( + mSelectionHour + + (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP) + ) + } + false + } + findSelectedEvent(x, y) + sendAccessibilityEventAsNeeded(true) + + // Restore old values + if (keepOldSelection) { + mSelectedEvent = savedEvent + mSelectionDay = savedDay + mSelectionHour = savedHour + mSelectionAllday = savedAllDay + } + return true + } + + private fun findSelectedEvent(x: Int, y: Int) { + var y = y + val date = mSelectionDay + val cellWidth = mCellWidth + var events: ArrayList<Event>? = mEvents + var numEvents: Int = events!!.size + val left = computeDayLeftPosition(mSelectionDay - mFirstJulianDay) + val top = 0 + setSelectedEvent(null) + mSelectedEvents.clear() + if (mSelectionAllday) { + var yDistance: Float + var minYdistance = 10000.0f // any large number + var closestEvent: Event? = null + val drawHeight = mAlldayHeight.toFloat() + val yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN + var maxUnexpandedColumn = mMaxUnexpandedAlldayEventCount + if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { + // Leave a gap for the 'box +n' text + maxUnexpandedColumn-- + } + events = mAllDayEvents + numEvents = events!!.size + for (i in 0 until numEvents) { + val event: Event? = events?.get(i) + if (!event!!.drawAsAllday() || + !mShowAllAllDayEvents && event!!.getColumn() >= maxUnexpandedColumn + ) { + // Don't check non-allday events or events that aren't shown + continue + } + if (event!!.startDay <= mSelectionDay && event!!.endDay >= mSelectionDay) { + val numRectangles = + if (mShowAllAllDayEvents) mMaxAlldayEvents.toFloat() + else mMaxUnexpandedAlldayEventCount.toFloat() + var height = drawHeight / numRectangles + if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { + height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT.toFloat() + } + val eventTop: Float = yOffset + height * event?.getColumn() + val eventBottom = eventTop + height + if (eventTop < y && eventBottom > y) { + // If the touch is inside the event rectangle, then + // add the event. + mSelectedEvents.add(event) + closestEvent = event + break + } else { + // Find the closest event + yDistance = if (eventTop >= y) { + eventTop - y + } else { + y - eventBottom + } + if (yDistance < minYdistance) { + minYdistance = yDistance + closestEvent = event + } + } + } + } + setSelectedEvent(closestEvent) + return + } + + // Adjust y for the scrollable bitmap + y += mViewStartY - mFirstCell + + // Use a region around (x,y) for the selection region + val region: Rect = mRect + region.left = x - 10 + region.right = x + 10 + region.top = y - 10 + region.bottom = y + 10 + val geometry: EventGeometry = mEventGeometry + for (i in 0 until numEvents) { + val event: Event? = events?.get(i) + // Compute the event rectangle. + if (!geometry.computeEventRect(date, left, top, cellWidth, event as Event)) { + continue + } + + // If the event intersects the selection region, then add it to + // mSelectedEvents. + if (geometry.eventIntersectsSelection(event as Event, region)) { + mSelectedEvents.add(event as Event) + } + } + + // If there are any events in the selected region, then assign the + // closest one to mSelectedEvent. + if (mSelectedEvents.size > 0) { + val len: Int = mSelectedEvents.size + var closestEvent: Event? = null + var minDist = (mViewWidth + mViewHeight).toFloat() // some large distance + for (index in 0 until len) { + val ev: Event? = mSelectedEvents?.get(index) + val dist: Float = geometry.pointToEvent(x.toFloat(), y.toFloat(), ev as Event) + if (dist < minDist) { + minDist = dist + closestEvent = ev + } + } + setSelectedEvent(closestEvent) + + // Keep the selected hour and day consistent with the selected + // event. They could be different if we touched on an empty hour + // slot very close to an event in the previous hour slot. In + // that case we will select the nearby event. + val startDay: Int = mSelectedEvent!!.startDay + val endDay: Int = mSelectedEvent!!.endDay + if (mSelectionDay < startDay) { + setSelectedDay(startDay) + } else if (mSelectionDay > endDay) { + setSelectedDay(endDay) + } + val startHour: Int = mSelectedEvent!!.startTime / 60 + val endHour: Int + endHour = if (mSelectedEvent!!.startTime < mSelectedEvent!!.endTime) { + (mSelectedEvent!!.endTime - 1) / 60 + } else { + mSelectedEvent!!.endTime / 60 + } + if (mSelectionHour < startHour && mSelectionDay == startDay) { + setSelectedHour(startHour) + } else if (mSelectionHour > endHour && mSelectionDay == endDay) { + setSelectedHour(endHour) + } + } + } + + // Encapsulates the code to continue the scrolling after the + // finger is lifted. Instead of stopping the scroll immediately, + // the scroll continues to "free spin" and gradually slows down. + private inner class ContinueScroll : Runnable { + override fun run() { + mScrolling = mScrolling && mScroller.computeScrollOffset() + if (!mScrolling || mPaused) { + resetSelectedHour() + invalidate() + return + } + mViewStartY = mScroller.getCurrY() + if (mCallEdgeEffectOnAbsorb) { + if (mViewStartY < 0) { + mEdgeEffectTop.onAbsorb(mLastVelocity.toInt()) + mCallEdgeEffectOnAbsorb = false + } else if (mViewStartY > mMaxViewStartY) { + mEdgeEffectBottom.onAbsorb(mLastVelocity.toInt()) + mCallEdgeEffectOnAbsorb = false + } + mLastVelocity = mScroller.getCurrVelocity() + } + if (mScrollStartY == 0 || mScrollStartY == mMaxViewStartY) { + // Allow overscroll/springback only on a fling, + // not a pull/fling from the end + if (mViewStartY < 0) { + mViewStartY = 0 + } else if (mViewStartY > mMaxViewStartY) { + mViewStartY = mMaxViewStartY + } + } + computeFirstHour() + mHandler?.post(this) + invalidate() + } + } + + /** + * Cleanup the pop-up and timers. + */ + fun cleanup() { + // Protect against null-pointer exceptions + if (mPopup != null) { + mPopup?.dismiss() + } + mPaused = true + mLastPopupEventID = INVALID_EVENT_ID + if (mHandler != null) { + mHandler?.removeCallbacks(mDismissPopup) + mHandler?.removeCallbacks(mUpdateCurrentTime) + } + Utils.setSharedPreference( + mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, + mCellHeight + ) + // Clear all click animations + eventClickCleanup() + // Turn off redraw + mRemeasure = false + // Turn off scrolling to make sure the view is in the correct state if we fling back to it + mScrolling = false + } + + private fun eventClickCleanup() { + this.removeCallbacks(mClearClick) + this.removeCallbacks(mSetClick) + mClickedEvent = null + mSavedClickedEvent = null + } + + private fun setSelectedEvent(e: Event?) { + mSelectedEvent = e + mSelectedEventForAccessibility = e + } + + private fun setSelectedHour(h: Int) { + mSelectionHour = h + mSelectionHourForAccessibility = h + } + + private fun setSelectedDay(d: Int) { + mSelectionDay = d + mSelectionDayForAccessibility = d + } + + /** + * Restart the update timer + */ + fun restartCurrentTimeUpdates() { + mPaused = false + if (mHandler != null) { + mHandler?.removeCallbacks(mUpdateCurrentTime) + mHandler?.post(mUpdateCurrentTime) + } + } + + @Override + protected override fun onDetachedFromWindow() { + cleanup() + super.onDetachedFromWindow() + } + + internal inner class DismissPopup : Runnable { + override fun run() { + // Protect against null-pointer exceptions + if (mPopup != null) { + mPopup?.dismiss() + } + } + } + + internal inner class UpdateCurrentTime : Runnable { + override fun run() { + val currentTime: Long = System.currentTimeMillis() + mCurrentTime?.set(currentTime) + // % causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.) + if (!mPaused) { + mHandler?.postDelayed( + mUpdateCurrentTime, UPDATE_CURRENT_TIME_DELAY - + currentTime % UPDATE_CURRENT_TIME_DELAY + ) + } + mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime!!.gmtoff) + invalidate() + } + } + + internal inner class CalendarGestureListener : GestureDetector.SimpleOnGestureListener() { + @Override + override fun onSingleTapUp(ev: MotionEvent): Boolean { + if (DEBUG) Log.e(TAG, "GestureDetector.onSingleTapUp") + doSingleTapUp(ev) + return true + } + + @Override + override fun onLongPress(ev: MotionEvent) { + if (DEBUG) Log.e(TAG, "GestureDetector.onLongPress") + doLongPress(ev) + } + + @Override + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + var distanceY = distanceY + if (DEBUG) Log.e(TAG, "GestureDetector.onScroll") + eventClickCleanup() + if (mTouchStartedInAlldayArea) { + if (Math.abs(distanceX) < Math.abs(distanceY)) { + // Make sure that click feedback is gone when you scroll from the + // all day area + invalidate() + return false + } + // don't scroll vertically if this started in the allday area + distanceY = 0f + } + doScroll(e1, e2, distanceX, distanceY) + return true + } + + @Override + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + var velocityY = velocityY + if (DEBUG) Log.e(TAG, "GestureDetector.onFling") + if (mTouchStartedInAlldayArea) { + if (Math.abs(velocityX) < Math.abs(velocityY)) { + return false + } + // don't fling vertically if this started in the allday area + velocityY = 0f + } + doFling(e1, e2, velocityX, velocityY) + return true + } + + @Override + override fun onDown(ev: MotionEvent): Boolean { + if (DEBUG) Log.e(TAG, "GestureDetector.onDown") + doDown(ev) + return true + } + } + + @Override + override fun onLongClick(v: View?): Boolean { + return true + } + + private inner class ScrollInterpolator : Interpolator { + override fun getInterpolation(t: Float): Float { + var t = t + t -= 1.0f + t = t * t * t * t * t + 1 + if ((1 - t) * mAnimationDistance < 1) { + cancelAnimation() + } + return t + } + } + + private fun calculateDuration(delta: Float, width: Float, velocity: Float): Long { + /* + * Here we compute a "distance" that will be used in the computation of + * the overall snap duration. This is a function of the actual distance + * that needs to be traveled; we keep this value close to half screen + * size in order to reduce the variance in snap duration as a function + * of the distance the page needs to travel. + */ + var velocity = velocity + val halfScreenSize = width / 2 + val distanceRatio = delta / width + val distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio) + val distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration + velocity = Math.abs(velocity) + velocity = Math.max(MINIMUM_SNAP_VELOCITY.toFloat(), velocity) + + /* + * we want the page's snap velocity to approximately match the velocity + * at which the user flings, so we scale the duration by a value near to + * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to + * make it a little slower. + */ + val duration: Long = 6L * Math.round(1000 * Math.abs(distance / velocity)) + if (DEBUG) { + Log.e( + TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:" + + distanceRatio + " distance:" + distance + " velocity:" + velocity + + " duration:" + duration + " distanceInfluenceForSnapDuration:" + + distanceInfluenceForSnapDuration + ) + } + return duration + } + + /* + * We want the duration of the page snap animation to be influenced by the + * distance that the screen has to travel, however, we don't want this + * duration to be effected in a purely linear fashion. Instead, we use this + * method to moderate the effect that the distance of travel has on the + * overall snap duration. + */ + private fun distanceInfluenceForSnapDuration(f: Float): Float { + var f = f + f -= 0.5f // center the values about 0. + f *= (0.3f * Math.PI / 2.0f).toFloat() + return Math.sin(f.toDouble()).toFloat() + } + + companion object { + private const val TAG = "DayView" + private const val DEBUG = false + private const val DEBUG_SCALING = false + private const val PERIOD_SPACE = ". " + private var mScale = 0f // Used for supporting different screen densities + private const val INVALID_EVENT_ID: Long = -1 // This is used for remembering a null event + + // Duration of the allday expansion + private const val ANIMATION_DURATION: Long = 400 + + // duration of the more allday event text fade + private const val ANIMATION_SECONDARY_DURATION: Long = 200 + + // duration of the scroll to go to a specified time + private const val GOTO_SCROLL_DURATION = 200 + + // duration for events' cross-fade animation + private const val EVENTS_CROSS_FADE_DURATION = 400 + + // duration to show the event clicked + private const val CLICK_DISPLAY_DURATION = 50 + private const val MENU_DAY = 3 + private const val MENU_EVENT_VIEW = 5 + private const val MENU_EVENT_CREATE = 6 + private const val MENU_EVENT_EDIT = 7 + private const val MENU_EVENT_DELETE = 8 + private var DEFAULT_CELL_HEIGHT = 64 + private var MAX_CELL_HEIGHT = 150 + private var MIN_Y_SPAN = 100 + private val CALENDARS_PROJECTION = arrayOf<String>( + Calendars._ID, // 0 + Calendars.CALENDAR_ACCESS_LEVEL, // 1 + Calendars.OWNER_ACCOUNT + ) + private const val CALENDARS_INDEX_ACCESS_LEVEL = 1 + private const val CALENDARS_INDEX_OWNER_ACCOUNT = 2 + private val CALENDARS_WHERE: String = Calendars._ID.toString() + "=%d" + private const val FROM_NONE = 0 + private const val FROM_ABOVE = 1 + private const val FROM_BELOW = 2 + private const val FROM_LEFT = 4 + private const val FROM_RIGHT = 8 + private const val ACCESS_LEVEL_NONE = 0 + private const val ACCESS_LEVEL_DELETE = 1 + private const val ACCESS_LEVEL_EDIT = 2 + private var mHorizontalSnapBackThreshold = 128 + + // Update the current time line every five minutes if the window is left open that long + private const val UPDATE_CURRENT_TIME_DELAY = 300000 + private var mOnDownDelay = 0 + protected var mStringBuilder: StringBuilder = StringBuilder(50) + + // TODO recreate formatter when locale changes + protected var mFormatter: Formatter = Formatter(mStringBuilder, Locale.getDefault()) + + // The number of milliseconds to show the popup window + private const val POPUP_DISMISS_DELAY = 3000 + private var GRID_LINE_LEFT_MARGIN = 0f + private const val GRID_LINE_INNER_WIDTH = 1f + private const val DAY_GAP = 1 + private const val HOUR_GAP = 1 + + // This is the standard height of an allday event with no restrictions + private var SINGLE_ALLDAY_HEIGHT = 34 + + /** + * This is the minimum desired height of a allday event. + * When unexpanded, allday events will use this height. + * When expanded allDay events will attempt to grow to fit all + * events at this height. + */ + private var MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = 28.0f // in pixels + + /** + * This is how big the unexpanded allday height is allowed to be. + * It will get adjusted based on screen size + */ + private var MAX_UNEXPANDED_ALLDAY_HEIGHT = (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4).toInt() + + /** + * This is the minimum size reserved for displaying regular events. + * The expanded allDay region can't expand into this. + */ + private const val MIN_HOURS_HEIGHT = 180 + private var ALLDAY_TOP_MARGIN = 1 + + // The largest a single allDay event will become. + private var MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34 + private var HOURS_TOP_MARGIN = 2 + private var HOURS_LEFT_MARGIN = 2 + private var HOURS_RIGHT_MARGIN = 4 + private var HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN + private var NEW_EVENT_MARGIN = 4 + private var NEW_EVENT_WIDTH = 2 + private var NEW_EVENT_MAX_LENGTH = 16 + private var CURRENT_TIME_LINE_SIDE_BUFFER = 4 + private var CURRENT_TIME_LINE_TOP_OFFSET = 2 + + /* package */ + const val MINUTES_PER_HOUR = 60 + + /* package */ + const val MINUTES_PER_DAY = MINUTES_PER_HOUR * 24 + + /* package */ + const val MILLIS_PER_MINUTE = 60 * 1000 + + /* package */ + const val MILLIS_PER_HOUR = 3600 * 1000 + + /* package */ + const val MILLIS_PER_DAY = MILLIS_PER_HOUR * 24 + + // More events text will transition between invisible and this alpha + private const val MORE_EVENTS_MAX_ALPHA = 0x4C + private var DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0 + private var DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5 + private var DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6 + private var DAY_HEADER_RIGHT_MARGIN = 4 + private var DAY_HEADER_BOTTOM_MARGIN = 3 + private var DAY_HEADER_FONT_SIZE = 14f + private var DATE_HEADER_FONT_SIZE = 32f + private var NORMAL_FONT_SIZE = 12f + private var EVENT_TEXT_FONT_SIZE = 12f + private var HOURS_TEXT_SIZE = 12f + private var AMPM_TEXT_SIZE = 9f + private var MIN_HOURS_WIDTH = 96 + private var MIN_CELL_WIDTH_FOR_TEXT = 20 + private const val MAX_EVENT_TEXT_LEN = 500 + + // smallest height to draw an event with + private var MIN_EVENT_HEIGHT = 24.0f // in pixels + private var CALENDAR_COLOR_SQUARE_SIZE = 10 + private var EVENT_RECT_TOP_MARGIN = 1 + private var EVENT_RECT_BOTTOM_MARGIN = 0 + private var EVENT_RECT_LEFT_MARGIN = 1 + private var EVENT_RECT_RIGHT_MARGIN = 0 + private var EVENT_RECT_STROKE_WIDTH = 2 + private var EVENT_TEXT_TOP_MARGIN = 2 + private var EVENT_TEXT_BOTTOM_MARGIN = 2 + private var EVENT_TEXT_LEFT_MARGIN = 6 + private var EVENT_TEXT_RIGHT_MARGIN = 6 + private var ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1 + private var EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN + private var EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN + private var EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN + private var EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN + + // margins and sizing for the expand allday icon + private var EXPAND_ALL_DAY_BOTTOM_MARGIN = 10 + + // sizing for "box +n" in allDay events + private var EVENT_SQUARE_WIDTH = 10 + private var EVENT_LINE_PADDING = 4 + private var NEW_EVENT_HINT_FONT_SIZE = 12 + private var mEventTextColor = 0 + private var mMoreEventsTextColor = 0 + private var mWeek_saturdayColor = 0 + private var mWeek_sundayColor = 0 + private var mCalendarDateBannerTextColor = 0 + private var mCalendarAmPmLabel = 0 + private var mCalendarGridAreaSelected = 0 + private var mCalendarGridLineInnerHorizontalColor = 0 + private var mCalendarGridLineInnerVerticalColor = 0 + private var mFutureBgColor = 0 + private var mFutureBgColorRes = 0 + private var mBgColor = 0 + private var mNewEventHintColor = 0 + private var mCalendarHourLabelColor = 0 + private var mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA + private var mCellHeight = 0 // shared among all DayViews + private var mMinCellHeight = 32 + private var mScaledPagingTouchSlop = 0 + + /** + * Whether to use the expand or collapse icon. + */ + private var mUseExpandIcon = true + + /** + * The height of the day names/numbers + */ + private var DAY_HEADER_HEIGHT = 45 + + /** + * The height of the day names/numbers for multi-day views + */ + private var MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT + + /** + * The height of the day names/numbers when viewing a single day + */ + private var ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT + + /** + * Whether or not to expand the allDay area to fill the screen + */ + private var mShowAllAllDayEvents = false + private var sCounter = 0 + + /** + * The initial state of the touch mode when we enter this view. + */ + private const val TOUCH_MODE_INITIAL_STATE = 0 + + /** + * Indicates we just received the touch event and we are waiting to see if + * it is a tap or a scroll gesture. + */ + private const val TOUCH_MODE_DOWN = 1 + + /** + * Indicates the touch gesture is a vertical scroll + */ + private const val TOUCH_MODE_VSCROLL = 0x20 + + /** + * Indicates the touch gesture is a horizontal scroll + */ + private const val TOUCH_MODE_HSCROLL = 0x40 + + /** + * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS. + */ + private const val SELECTION_HIDDEN = 0 + private const val SELECTION_PRESSED = 1 // D-pad down but not up yet + private const val SELECTION_SELECTED = 2 + private const val SELECTION_LONGPRESS = 3 + + // The rest of this file was borrowed from Launcher2 - PagedView.java + private const val MINIMUM_SNAP_VELOCITY = 2200 + } + + init { + mContext = context + initAccessibilityVariables() + mResources = context!!.getResources() + mNewEventHintString = mResources.getString(R.string.day_view_new_event_hint) + mNumDays = numDays + DATE_HEADER_FONT_SIZE = + mResources.getDimension(R.dimen.date_header_text_size).toInt().toFloat() + DAY_HEADER_FONT_SIZE = + mResources.getDimension(R.dimen.day_label_text_size).toInt().toFloat() + ONE_DAY_HEADER_HEIGHT = mResources.getDimension(R.dimen.one_day_header_height).toInt() + DAY_HEADER_BOTTOM_MARGIN = mResources.getDimension(R.dimen.day_header_bottom_margin).toInt() + EXPAND_ALL_DAY_BOTTOM_MARGIN = + mResources.getDimension(R.dimen.all_day_bottom_margin).toInt() + HOURS_TEXT_SIZE = mResources.getDimension(R.dimen.hours_text_size).toInt().toFloat() + AMPM_TEXT_SIZE = mResources.getDimension(R.dimen.ampm_text_size).toInt().toFloat() + MIN_HOURS_WIDTH = mResources.getDimension(R.dimen.min_hours_width).toInt() + HOURS_LEFT_MARGIN = mResources.getDimension(R.dimen.hours_left_margin).toInt() + HOURS_RIGHT_MARGIN = mResources.getDimension(R.dimen.hours_right_margin).toInt() + MULTI_DAY_HEADER_HEIGHT = mResources.getDimension(R.dimen.day_header_height).toInt() + val eventTextSizeId: Int + eventTextSizeId = if (mNumDays == 1) { + R.dimen.day_view_event_text_size + } else { + R.dimen.week_view_event_text_size + } + EVENT_TEXT_FONT_SIZE = mResources.getDimension(eventTextSizeId).toFloat() + NEW_EVENT_HINT_FONT_SIZE = mResources.getDimension(R.dimen.new_event_hint_text_size).toInt() + MIN_EVENT_HEIGHT = mResources.getDimension(R.dimen.event_min_height) + MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = MIN_EVENT_HEIGHT + EVENT_TEXT_TOP_MARGIN = mResources.getDimension(R.dimen.event_text_vertical_margin).toInt() + EVENT_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_LEFT_MARGIN = mResources + .getDimension(R.dimen.event_text_horizontal_margin).toInt() + EVENT_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN + if (mScale == 0f) { + mScale = mResources.getDisplayMetrics().density + if (mScale != 1f) { + SINGLE_ALLDAY_HEIGHT *= mScale.toInt() + ALLDAY_TOP_MARGIN *= mScale.toInt() + MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale.toInt() + NORMAL_FONT_SIZE *= mScale + GRID_LINE_LEFT_MARGIN *= mScale + HOURS_TOP_MARGIN *= mScale.toInt() + MIN_CELL_WIDTH_FOR_TEXT *= mScale.toInt() + MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale.toInt() + mAnimateDayEventHeight = MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT.toInt() + CURRENT_TIME_LINE_SIDE_BUFFER *= mScale.toInt() + CURRENT_TIME_LINE_TOP_OFFSET *= mScale.toInt() + MIN_Y_SPAN *= mScale.toInt() + MAX_CELL_HEIGHT *= mScale.toInt() + DEFAULT_CELL_HEIGHT *= mScale.toInt() + DAY_HEADER_HEIGHT *= mScale.toInt() + DAY_HEADER_RIGHT_MARGIN *= mScale.toInt() + DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale.toInt() + DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale.toInt() + DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale.toInt() + CALENDAR_COLOR_SQUARE_SIZE *= mScale.toInt() + EVENT_RECT_TOP_MARGIN *= mScale.toInt() + EVENT_RECT_BOTTOM_MARGIN *= mScale.toInt() + ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale.toInt() + EVENT_RECT_LEFT_MARGIN *= mScale.toInt() + EVENT_RECT_RIGHT_MARGIN *= mScale.toInt() + EVENT_RECT_STROKE_WIDTH *= mScale.toInt() + EVENT_SQUARE_WIDTH *= mScale.toInt() + EVENT_LINE_PADDING *= mScale.toInt() + NEW_EVENT_MARGIN *= mScale.toInt() + NEW_EVENT_WIDTH *= mScale.toInt() + NEW_EVENT_MAX_LENGTH *= mScale.toInt() + } + } + HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN + DAY_HEADER_HEIGHT = if (mNumDays == 1) ONE_DAY_HEADER_HEIGHT else MULTI_DAY_HEADER_HEIGHT + mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_indicator_holo_light) + mCurrentTimeAnimateLine = mResources + .getDrawable(R.drawable.timeline_indicator_activated_holo_light) + mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light) + mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_expand_holo_light) + mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_collapse_holo_light) + mNewEventHintColor = mResources.getColor(R.color.new_event_hint_text_color) + mAcceptedOrTentativeEventBoxDrawable = mResources + .getDrawable(R.drawable.panel_month_event_holo_light) + mEventLoader = eventLoader as EventLoader + mEventGeometry = EventGeometry() + mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT) + mEventGeometry.setHourGap(HOUR_GAP.toFloat()) + mEventGeometry.setCellMargin(DAY_GAP) + mLastPopupEventID = INVALID_EVENT_ID + mController = controller as CalendarController + mViewSwitcher = viewSwitcher as ViewSwitcher + mGestureDetector = GestureDetector(context, CalendarGestureListener()) + mScaleGestureDetector = ScaleGestureDetector(getContext(), this) + if (mCellHeight == 0) { + mCellHeight = Utils.getSharedPreference( + mContext, + GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT + ) + } + mScroller = OverScroller(context) + mHScrollInterpolator = ScrollInterpolator() + mEdgeEffectTop = EdgeEffect(context) + mEdgeEffectBottom = EdgeEffect(context) + val vc: ViewConfiguration = ViewConfiguration.get(context) + mScaledPagingTouchSlop = vc.getScaledPagingTouchSlop() + mOnDownDelay = ViewConfiguration.getTapTimeout() + OVERFLING_DISTANCE = vc.getScaledOverflingDistance() + init(context as Context) + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/Event.java b/src/com/android/calendar/Event.java deleted file mode 100644 index 095e43e7..00000000 --- a/src/com/android/calendar/Event.java +++ /dev/null @@ -1,642 +0,0 @@ -/* - * Copyright (C) 2007 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.calendar; - -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.database.Cursor; -import android.net.Uri; -import android.os.Debug; -import android.provider.CalendarContract.Attendees; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Events; -import android.provider.CalendarContract.Instances; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.concurrent.atomic.AtomicInteger; - -// TODO: should Event be Parcelable so it can be passed via Intents? -public class Event implements Cloneable { - - private static final String TAG = "CalEvent"; - private static final boolean PROFILE = false; - - /** - * The sort order is: - * 1) events with an earlier start (begin for normal events, startday for allday) - * 2) events with a later end (end for normal events, endday for allday) - * 3) the title (unnecessary, but nice) - * - * The start and end day is sorted first so that all day events are - * sorted correctly with respect to events that are >24 hours (and - * therefore show up in the allday area). - */ - private static final String SORT_EVENTS_BY = - "begin ASC, end DESC, title ASC"; - private static final String SORT_ALLDAY_BY = - "startDay ASC, endDay DESC, title ASC"; - private static final String DISPLAY_AS_ALLDAY = "dispAllday"; - - private static final String EVENTS_WHERE = DISPLAY_AS_ALLDAY + "=0"; - private static final String ALLDAY_WHERE = DISPLAY_AS_ALLDAY + "=1"; - - // The projection to use when querying instances to build a list of events - public static final String[] EVENT_PROJECTION = new String[] { - Instances.TITLE, // 0 - Instances.EVENT_LOCATION, // 1 - Instances.ALL_DAY, // 2 - Instances.DISPLAY_COLOR, // 3 If SDK < 16, set to Instances.CALENDAR_COLOR. - Instances.EVENT_TIMEZONE, // 4 - Instances.EVENT_ID, // 5 - Instances.BEGIN, // 6 - Instances.END, // 7 - Instances._ID, // 8 - Instances.START_DAY, // 9 - Instances.END_DAY, // 10 - Instances.START_MINUTE, // 11 - Instances.END_MINUTE, // 12 - Instances.HAS_ALARM, // 13 - Instances.RRULE, // 14 - Instances.RDATE, // 15 - Instances.SELF_ATTENDEE_STATUS, // 16 - Events.ORGANIZER, // 17 - Events.GUESTS_CAN_MODIFY, // 18 - Instances.ALL_DAY + "=1 OR (" + Instances.END + "-" + Instances.BEGIN + ")>=" - + DateUtils.DAY_IN_MILLIS + " AS " + DISPLAY_AS_ALLDAY, // 19 - }; - - // The indices for the projection array above. - private static final int PROJECTION_TITLE_INDEX = 0; - private static final int PROJECTION_LOCATION_INDEX = 1; - private static final int PROJECTION_ALL_DAY_INDEX = 2; - private static final int PROJECTION_COLOR_INDEX = 3; - private static final int PROJECTION_TIMEZONE_INDEX = 4; - private static final int PROJECTION_EVENT_ID_INDEX = 5; - private static final int PROJECTION_BEGIN_INDEX = 6; - private static final int PROJECTION_END_INDEX = 7; - private static final int PROJECTION_START_DAY_INDEX = 9; - private static final int PROJECTION_END_DAY_INDEX = 10; - private static final int PROJECTION_START_MINUTE_INDEX = 11; - private static final int PROJECTION_END_MINUTE_INDEX = 12; - private static final int PROJECTION_HAS_ALARM_INDEX = 13; - private static final int PROJECTION_RRULE_INDEX = 14; - private static final int PROJECTION_RDATE_INDEX = 15; - private static final int PROJECTION_SELF_ATTENDEE_STATUS_INDEX = 16; - private static final int PROJECTION_ORGANIZER_INDEX = 17; - private static final int PROJECTION_GUESTS_CAN_INVITE_OTHERS_INDEX = 18; - private static final int PROJECTION_DISPLAY_AS_ALLDAY = 19; - - static { - if (!Utils.isJellybeanOrLater()) { - EVENT_PROJECTION[PROJECTION_COLOR_INDEX] = Instances.CALENDAR_COLOR; - } - } - - private static String mNoTitleString; - private static int mNoColorColor; - - public long id; - public int color; - public CharSequence title; - public CharSequence location; - public boolean allDay; - public String organizer; - public boolean guestsCanModify; - - public int startDay; // start Julian day - public int endDay; // end Julian day - public int startTime; // Start and end time are in minutes since midnight - public int endTime; - - public long startMillis; // UTC milliseconds since the epoch - public long endMillis; // UTC milliseconds since the epoch - private int mColumn; - private int mMaxColumns; - - public boolean hasAlarm; - public boolean isRepeating; - - public int selfAttendeeStatus; - - // The coordinates of the event rectangle drawn on the screen. - public float left; - public float right; - public float top; - public float bottom; - - // These 4 fields are used for navigating among events within the selected - // hour in the Day and Week view. - public Event nextRight; - public Event nextLeft; - public Event nextUp; - public Event nextDown; - - @Override - public final Object clone() throws CloneNotSupportedException { - super.clone(); - Event e = new Event(); - - e.title = title; - e.color = color; - e.location = location; - e.allDay = allDay; - e.startDay = startDay; - e.endDay = endDay; - e.startTime = startTime; - e.endTime = endTime; - e.startMillis = startMillis; - e.endMillis = endMillis; - e.hasAlarm = hasAlarm; - e.isRepeating = isRepeating; - e.selfAttendeeStatus = selfAttendeeStatus; - e.organizer = organizer; - e.guestsCanModify = guestsCanModify; - - return e; - } - - public final void copyTo(Event dest) { - dest.id = id; - dest.title = title; - dest.color = color; - dest.location = location; - dest.allDay = allDay; - dest.startDay = startDay; - dest.endDay = endDay; - dest.startTime = startTime; - dest.endTime = endTime; - dest.startMillis = startMillis; - dest.endMillis = endMillis; - dest.hasAlarm = hasAlarm; - dest.isRepeating = isRepeating; - dest.selfAttendeeStatus = selfAttendeeStatus; - dest.organizer = organizer; - dest.guestsCanModify = guestsCanModify; - } - - public static final Event newInstance() { - Event e = new Event(); - - e.id = 0; - e.title = null; - e.color = 0; - e.location = null; - e.allDay = false; - e.startDay = 0; - e.endDay = 0; - e.startTime = 0; - e.endTime = 0; - e.startMillis = 0; - e.endMillis = 0; - e.hasAlarm = false; - e.isRepeating = false; - e.selfAttendeeStatus = Attendees.ATTENDEE_STATUS_NONE; - - return e; - } - - /** - * Loads <i>days</i> days worth of instances starting at <i>startDay</i>. - */ - public static void loadEvents(Context context, ArrayList<Event> events, int startDay, int days, - int requestId, AtomicInteger sequenceNumber) { - - if (PROFILE) { - Debug.startMethodTracing("loadEvents"); - } - - Cursor cEvents = null; - Cursor cAllday = null; - - events.clear(); - try { - int endDay = startDay + days - 1; - - // We use the byDay instances query to get a list of all events for - // the days we're interested in. - // The sort order is: events with an earlier start time occur - // first and if the start times are the same, then events with - // a later end time occur first. The later end time is ordered - // first so that long rectangles in the calendar views appear on - // the left side. If the start and end times of two events are - // the same then we sort alphabetically on the title. This isn't - // required for correctness, it just adds a nice touch. - - // Respect the preference to show/hide declined events - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - boolean hideDeclined = prefs.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, - false); - - String where = EVENTS_WHERE; - String whereAllday = ALLDAY_WHERE; - if (hideDeclined) { - String hideString = " AND " + Instances.SELF_ATTENDEE_STATUS + "!=" - + Attendees.ATTENDEE_STATUS_DECLINED; - where += hideString; - whereAllday += hideString; - } - - cEvents = instancesQuery(context.getContentResolver(), EVENT_PROJECTION, startDay, - endDay, where, null, SORT_EVENTS_BY); - cAllday = instancesQuery(context.getContentResolver(), EVENT_PROJECTION, startDay, - endDay, whereAllday, null, SORT_ALLDAY_BY); - - // Check if we should return early because there are more recent - // load requests waiting. - if (requestId != sequenceNumber.get()) { - return; - } - - buildEventsFromCursor(events, cEvents, context, startDay, endDay); - buildEventsFromCursor(events, cAllday, context, startDay, endDay); - - } finally { - if (cEvents != null) { - cEvents.close(); - } - if (cAllday != null) { - cAllday.close(); - } - if (PROFILE) { - Debug.stopMethodTracing(); - } - } - } - - /** - * Performs a query to return all visible instances in the given range - * that match the given selection. This is a blocking function and - * should not be done on the UI thread. This will cause an expansion of - * recurring events to fill this time range if they are not already - * expanded and will slow down for larger time ranges with many - * recurring events. - * - * @param cr The ContentResolver to use for the query - * @param projection The columns to return - * @param begin The start of the time range to query in UTC millis since - * epoch - * @param end The end of the time range to query in UTC millis since - * epoch - * @param selection Filter on the query as an SQL WHERE statement - * @param selectionArgs Args to replace any '?'s in the selection - * @param orderBy How to order the rows as an SQL ORDER BY statement - * @return A Cursor of instances matching the selection - */ - private static final Cursor instancesQuery(ContentResolver cr, String[] projection, - int startDay, int endDay, String selection, String[] selectionArgs, String orderBy) { - String WHERE_CALENDARS_SELECTED = Calendars.VISIBLE + "=?"; - String[] WHERE_CALENDARS_ARGS = {"1"}; - String DEFAULT_SORT_ORDER = "begin ASC"; - - Uri.Builder builder = Instances.CONTENT_BY_DAY_URI.buildUpon(); - ContentUris.appendId(builder, startDay); - ContentUris.appendId(builder, endDay); - if (TextUtils.isEmpty(selection)) { - selection = WHERE_CALENDARS_SELECTED; - selectionArgs = WHERE_CALENDARS_ARGS; - } else { - selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED; - if (selectionArgs != null && selectionArgs.length > 0) { - selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1); - selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0]; - } else { - selectionArgs = WHERE_CALENDARS_ARGS; - } - } - return cr.query(builder.build(), projection, selection, selectionArgs, - orderBy == null ? DEFAULT_SORT_ORDER : orderBy); - } - - /** - * Adds all the events from the cursors to the events list. - * - * @param events The list of events - * @param cEvents Events to add to the list - * @param context - * @param startDay - * @param endDay - */ - public static void buildEventsFromCursor( - ArrayList<Event> events, Cursor cEvents, Context context, int startDay, int endDay) { - if (cEvents == null || events == null) { - Log.e(TAG, "buildEventsFromCursor: null cursor or null events list!"); - return; - } - - int count = cEvents.getCount(); - - if (count == 0) { - return; - } - - Resources res = context.getResources(); - mNoTitleString = res.getString(R.string.no_title_label); - mNoColorColor = res.getColor(R.color.event_center); - // Sort events in two passes so we ensure the allday and standard events - // get sorted in the correct order - cEvents.moveToPosition(-1); - while (cEvents.moveToNext()) { - Event e = generateEventFromCursor(cEvents); - if (e.startDay > endDay || e.endDay < startDay) { - continue; - } - events.add(e); - } - } - - /** - * @param cEvents Cursor pointing at event - * @return An event created from the cursor - */ - private static Event generateEventFromCursor(Cursor cEvents) { - Event e = new Event(); - - e.id = cEvents.getLong(PROJECTION_EVENT_ID_INDEX); - e.title = cEvents.getString(PROJECTION_TITLE_INDEX); - e.location = cEvents.getString(PROJECTION_LOCATION_INDEX); - e.allDay = cEvents.getInt(PROJECTION_ALL_DAY_INDEX) != 0; - e.organizer = cEvents.getString(PROJECTION_ORGANIZER_INDEX); - e.guestsCanModify = cEvents.getInt(PROJECTION_GUESTS_CAN_INVITE_OTHERS_INDEX) != 0; - - if (e.title == null || e.title.length() == 0) { - e.title = mNoTitleString; - } - - if (!cEvents.isNull(PROJECTION_COLOR_INDEX)) { - // Read the color from the database - e.color = Utils.getDisplayColorFromColor(cEvents.getInt(PROJECTION_COLOR_INDEX)); - } else { - e.color = mNoColorColor; - } - - long eStart = cEvents.getLong(PROJECTION_BEGIN_INDEX); - long eEnd = cEvents.getLong(PROJECTION_END_INDEX); - - e.startMillis = eStart; - e.startTime = cEvents.getInt(PROJECTION_START_MINUTE_INDEX); - e.startDay = cEvents.getInt(PROJECTION_START_DAY_INDEX); - - e.endMillis = eEnd; - e.endTime = cEvents.getInt(PROJECTION_END_MINUTE_INDEX); - e.endDay = cEvents.getInt(PROJECTION_END_DAY_INDEX); - - e.hasAlarm = cEvents.getInt(PROJECTION_HAS_ALARM_INDEX) != 0; - - // Check if this is a repeating event - String rrule = cEvents.getString(PROJECTION_RRULE_INDEX); - String rdate = cEvents.getString(PROJECTION_RDATE_INDEX); - if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)) { - e.isRepeating = true; - } else { - e.isRepeating = false; - } - - e.selfAttendeeStatus = cEvents.getInt(PROJECTION_SELF_ATTENDEE_STATUS_INDEX); - return e; - } - - /** - * Computes a position for each event. Each event is displayed - * as a non-overlapping rectangle. For normal events, these rectangles - * are displayed in separate columns in the week view and day view. For - * all-day events, these rectangles are displayed in separate rows along - * the top. In both cases, each event is assigned two numbers: N, and - * Max, that specify that this event is the Nth event of Max number of - * events that are displayed in a group. The width and position of each - * rectangle depend on the maximum number of rectangles that occur at - * the same time. - * - * @param eventsList the list of events, sorted into increasing time order - * @param minimumDurationMillis minimum duration acceptable as cell height of each event - * rectangle in millisecond. Should be 0 when it is not determined. - */ - /* package */ static void computePositions(ArrayList<Event> eventsList, - long minimumDurationMillis) { - if (eventsList == null) { - return; - } - - // Compute the column positions separately for the all-day events - doComputePositions(eventsList, minimumDurationMillis, false); - doComputePositions(eventsList, minimumDurationMillis, true); - } - - private static void doComputePositions(ArrayList<Event> eventsList, - long minimumDurationMillis, boolean doAlldayEvents) { - final ArrayList<Event> activeList = new ArrayList<Event>(); - final ArrayList<Event> groupList = new ArrayList<Event>(); - - if (minimumDurationMillis < 0) { - minimumDurationMillis = 0; - } - - long colMask = 0; - int maxCols = 0; - for (Event event : eventsList) { - // Process all-day events separately - if (event.drawAsAllday() != doAlldayEvents) - continue; - - if (!doAlldayEvents) { - colMask = removeNonAlldayActiveEvents( - event, activeList.iterator(), minimumDurationMillis, colMask); - } else { - colMask = removeAlldayActiveEvents(event, activeList.iterator(), colMask); - } - - // If the active list is empty, then reset the max columns, clear - // the column bit mask, and empty the groupList. - if (activeList.isEmpty()) { - for (Event ev : groupList) { - ev.setMaxColumns(maxCols); - } - maxCols = 0; - colMask = 0; - groupList.clear(); - } - - // Find the first empty column. Empty columns are represented by - // zero bits in the column mask "colMask". - int col = findFirstZeroBit(colMask); - if (col == 64) - col = 63; - colMask |= (1L << col); - event.setColumn(col); - activeList.add(event); - groupList.add(event); - int len = activeList.size(); - if (maxCols < len) - maxCols = len; - } - for (Event ev : groupList) { - ev.setMaxColumns(maxCols); - } - } - - private static long removeAlldayActiveEvents(Event event, Iterator<Event> iter, long colMask) { - // Remove the inactive allday events. An event on the active list - // becomes inactive when the end day is less than the current event's - // start day. - while (iter.hasNext()) { - final Event active = iter.next(); - if (active.endDay < event.startDay) { - colMask &= ~(1L << active.getColumn()); - iter.remove(); - } - } - return colMask; - } - - private static long removeNonAlldayActiveEvents( - Event event, Iterator<Event> iter, long minDurationMillis, long colMask) { - long start = event.getStartMillis(); - // Remove the inactive events. An event on the active list - // becomes inactive when its end time is less than or equal to - // the current event's start time. - while (iter.hasNext()) { - final Event active = iter.next(); - - final long duration = Math.max( - active.getEndMillis() - active.getStartMillis(), minDurationMillis); - if ((active.getStartMillis() + duration) <= start) { - colMask &= ~(1L << active.getColumn()); - iter.remove(); - } - } - return colMask; - } - - public static int findFirstZeroBit(long val) { - for (int ii = 0; ii < 64; ++ii) { - if ((val & (1L << ii)) == 0) - return ii; - } - return 64; - } - - public final void dump() { - Log.e("Cal", "+-----------------------------------------+"); - Log.e("Cal", "+ id = " + id); - Log.e("Cal", "+ color = " + color); - Log.e("Cal", "+ title = " + title); - Log.e("Cal", "+ location = " + location); - Log.e("Cal", "+ allDay = " + allDay); - Log.e("Cal", "+ startDay = " + startDay); - Log.e("Cal", "+ endDay = " + endDay); - Log.e("Cal", "+ startTime = " + startTime); - Log.e("Cal", "+ endTime = " + endTime); - Log.e("Cal", "+ organizer = " + organizer); - Log.e("Cal", "+ guestwrt = " + guestsCanModify); - } - - public final boolean intersects(int julianDay, int startMinute, - int endMinute) { - if (endDay < julianDay) { - return false; - } - - if (startDay > julianDay) { - return false; - } - - if (endDay == julianDay) { - if (endTime < startMinute) { - return false; - } - // An event that ends at the start minute should not be considered - // as intersecting the given time span, but don't exclude - // zero-length (or very short) events. - if (endTime == startMinute - && (startTime != endTime || startDay != endDay)) { - return false; - } - } - - if (startDay == julianDay && startTime > endMinute) { - return false; - } - - return true; - } - - /** - * Returns the event title and location separated by a comma. If the - * location is already part of the title (at the end of the title), then - * just the title is returned. - * - * @return the event title and location as a String - */ - public String getTitleAndLocation() { - String text = title.toString(); - - // Append the location to the title, unless the title ends with the - // location (for example, "meeting in building 42" ends with the - // location). - if (location != null) { - String locationString = location.toString(); - if (!text.endsWith(locationString)) { - text += ", " + locationString; - } - } - return text; - } - - public void setColumn(int column) { - mColumn = column; - } - - public int getColumn() { - return mColumn; - } - - public void setMaxColumns(int maxColumns) { - mMaxColumns = maxColumns; - } - - public int getMaxColumns() { - return mMaxColumns; - } - - public void setStartMillis(long startMillis) { - this.startMillis = startMillis; - } - - public long getStartMillis() { - return startMillis; - } - - public void setEndMillis(long endMillis) { - this.endMillis = endMillis; - } - - public long getEndMillis() { - return endMillis; - } - - public boolean drawAsAllday() { - // Use >= so we'll pick up Exchange allday events - return allDay || endMillis - startMillis >= DateUtils.DAY_IN_MILLIS; - } -} diff --git a/src/com/android/calendar/Event.kt b/src/com/android/calendar/Event.kt new file mode 100644 index 00000000..c21a0a0e --- /dev/null +++ b/src/com/android/calendar/Event.kt @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.content.ContentResolver +import android.content.ContentUris +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Resources +import android.database.Cursor +import android.net.Uri +import android.os.Debug +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.Instances +import android.text.TextUtils +import android.text.format.DateUtils +import android.util.Log + +import java.util.ArrayList +import java.util.Arrays +import java.util.Iterator +import java.util.concurrent.atomic.AtomicInteger + +// TODO: should Event be Parcelable so it can be passed via Intents? +class Event : Cloneable { + companion object { + private const val TAG = "CalEvent" + private const val PROFILE = false + + /** + * The sort order is: + * 1) events with an earlier start (begin for normal events, startday for allday) + * 2) events with a later end (end for normal events, endday for allday) + * 3) the title (unnecessary, but nice) + * + * The start and end day is sorted first so that all day events are + * sorted correctly with respect to events that are >24 hours (and + * therefore show up in the allday area). + */ + private const val SORT_EVENTS_BY = "begin ASC, end DESC, title ASC" + private const val SORT_ALLDAY_BY = "startDay ASC, endDay DESC, title ASC" + private const val DISPLAY_AS_ALLDAY = "dispAllday" + private const val EVENTS_WHERE = DISPLAY_AS_ALLDAY + "=0" + private const val ALLDAY_WHERE = DISPLAY_AS_ALLDAY + "=1" + + // The projection to use when querying instances to build a list of events + @JvmField + val EVENT_PROJECTION = arrayOf<String>( + Instances.TITLE, // 0 + Instances.EVENT_LOCATION, // 1 + Instances.ALL_DAY, // 2 + Instances.DISPLAY_COLOR, // 3 If SDK < 16, set to Instances.CALENDAR_COLOR. + Instances.EVENT_TIMEZONE, // 4 + Instances.EVENT_ID, // 5 + Instances.BEGIN, // 6 + Instances.END, // 7 + Instances._ID, // 8 + Instances.START_DAY, // 9 + Instances.END_DAY, // 10 + Instances.START_MINUTE, // 11 + Instances.END_MINUTE, // 12 + Instances.HAS_ALARM, // 13 + Instances.RRULE, // 14 + Instances.RDATE, // 15 + Instances.SELF_ATTENDEE_STATUS, // 16 + Events.ORGANIZER, // 17 + Events.GUESTS_CAN_MODIFY, // 18 + Instances.ALL_DAY.toString() + "=1 OR (" + Instances.END + "-" + + Instances.BEGIN + ")>=" + + DateUtils.DAY_IN_MILLIS + " AS " + DISPLAY_AS_ALLDAY + ) + + // The indices for the projection array above. + private const val PROJECTION_TITLE_INDEX = 0 + private const val PROJECTION_LOCATION_INDEX = 1 + private const val PROJECTION_ALL_DAY_INDEX = 2 + private const val PROJECTION_COLOR_INDEX = 3 + private const val PROJECTION_TIMEZONE_INDEX = 4 + private const val PROJECTION_EVENT_ID_INDEX = 5 + private const val PROJECTION_BEGIN_INDEX = 6 + private const val PROJECTION_END_INDEX = 7 + private const val PROJECTION_START_DAY_INDEX = 9 + private const val PROJECTION_END_DAY_INDEX = 10 + private const val PROJECTION_START_MINUTE_INDEX = 11 + private const val PROJECTION_END_MINUTE_INDEX = 12 + private const val PROJECTION_HAS_ALARM_INDEX = 13 + private const val PROJECTION_RRULE_INDEX = 14 + private const val PROJECTION_RDATE_INDEX = 15 + private const val PROJECTION_SELF_ATTENDEE_STATUS_INDEX = 16 + private const val PROJECTION_ORGANIZER_INDEX = 17 + private const val PROJECTION_GUESTS_CAN_INVITE_OTHERS_INDEX = 18 + private const val PROJECTION_DISPLAY_AS_ALLDAY = 19 + private var mNoTitleString: String? = null + private var mNoColorColor = 0 + @JvmStatic fun newInstance(): Event { + val e = Event() + e.id = 0 + e.title = null + e.color = 0 + e.location = null + e.allDay = false + e.startDay = 0 + e.endDay = 0 + e.startTime = 0 + e.endTime = 0 + e.startMillis = 0 + e.endMillis = 0 + e.hasAlarm = false + e.isRepeating = false + e.selfAttendeeStatus = Attendees.ATTENDEE_STATUS_NONE + return e + } + + /** + * Loads *days* days worth of instances starting at *startDay*. + */ + @JvmStatic fun loadEvents( + context: Context?, + events: ArrayList<Event?>, + startDay: Int, + days: Int, + requestId: Int, + sequenceNumber: AtomicInteger? + ) { + if (PROFILE) { + Debug.startMethodTracing("loadEvents") + } + var cEvents: Cursor? = null + var cAllday: Cursor? = null + events.clear() + try { + val endDay = startDay + days - 1 + + // We use the byDay instances query to get a list of all events for + // the days we're interested in. + // The sort order is: events with an earlier start time occur + // first and if the start times are the same, then events with + // a later end time occur first. The later end time is ordered + // first so that long rectangles in the calendar views appear on + // the left side. If the start and end times of two events are + // the same then we sort alphabetically on the title. This isn't + // required for correctness, it just adds a nice touch. + + // Respect the preference to show/hide declined events + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + val hideDeclined: Boolean = prefs?.getBoolean( + GeneralPreferences.KEY_HIDE_DECLINED, + false + ) as Boolean + var where = EVENTS_WHERE + var whereAllday = ALLDAY_WHERE + if (hideDeclined) { + val hideString = (" AND " + Instances.SELF_ATTENDEE_STATUS.toString() + "!=" + + Attendees.ATTENDEE_STATUS_DECLINED) + where += hideString + whereAllday += hideString + } + cEvents = instancesQuery( + context?.getContentResolver(), EVENT_PROJECTION, startDay, + endDay, where, null, SORT_EVENTS_BY + ) + cAllday = instancesQuery( + context?.getContentResolver(), EVENT_PROJECTION, startDay, + endDay, whereAllday, null, SORT_ALLDAY_BY + ) + + // Check if we should return early because there are more recent + // load requests waiting. + if (requestId != sequenceNumber?.get()) { + return + } + buildEventsFromCursor(events, cEvents, context, startDay, endDay) + buildEventsFromCursor(events, cAllday, context, startDay, endDay) + } finally { + if (cEvents != null) { + cEvents.close() + } + if (cAllday != null) { + cAllday.close() + } + if (PROFILE) { + Debug.stopMethodTracing() + } + } + } + + /** + * Performs a query to return all visible instances in the given range + * that match the given selection. This is a blocking function and + * should not be done on the UI thread. This will cause an expansion of + * recurring events to fill this time range if they are not already + * expanded and will slow down for larger time ranges with many + * recurring events. + * + * @param cr The ContentResolver to use for the query + * @param projection The columns to return + * @param begin The start of the time range to query in UTC millis since + * epoch + * @param end The end of the time range to query in UTC millis since + * epoch + * @param selection Filter on the query as an SQL WHERE statement + * @param selectionArgs Args to replace any '?'s in the selection + * @param orderBy How to order the rows as an SQL ORDER BY statement + * @return A Cursor of instances matching the selection + */ + @JvmStatic private fun instancesQuery( + cr: ContentResolver?, + projection: Array<String>, + startDay: Int, + endDay: Int, + selection: String, + selectionArgs: Array<String?>?, + orderBy: String? + ): Cursor? { + var selection = selection + var selectionArgs = selectionArgs + val WHERE_CALENDARS_SELECTED: String = Calendars.VISIBLE.toString() + "=?" + val WHERE_CALENDARS_ARGS = arrayOf<String?>("1") + val DEFAULT_SORT_ORDER = "begin ASC" + val builder: Uri.Builder = Instances.CONTENT_BY_DAY_URI.buildUpon() + ContentUris.appendId(builder, startDay.toLong()) + ContentUris.appendId(builder, endDay.toLong()) + if (TextUtils.isEmpty(selection)) { + selection = WHERE_CALENDARS_SELECTED + selectionArgs = WHERE_CALENDARS_ARGS + } else { + selection = "($selection) AND $WHERE_CALENDARS_SELECTED" + if (selectionArgs != null && selectionArgs.size > 0) { + selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.size + 1) + selectionArgs[selectionArgs.size - 1] = WHERE_CALENDARS_ARGS[0] + } else { + selectionArgs = WHERE_CALENDARS_ARGS + } + } + return cr?.query( + builder.build(), projection, selection, selectionArgs, + orderBy ?: DEFAULT_SORT_ORDER + ) + } + + /** + * Adds all the events from the cursors to the events list. + * + * @param events The list of events + * @param cEvents Events to add to the list + * @param context + * @param startDay + * @param endDay + */ + @JvmStatic fun buildEventsFromCursor( + events: ArrayList<Event?>?, + cEvents: Cursor?, + context: Context?, + startDay: Int, + endDay: Int + ) { + if (cEvents == null || events == null) { + Log.e(TAG, "buildEventsFromCursor: null cursor or null events list!") + return + } + val count: Int = cEvents.getCount() + if (count == 0) { + return + } + val res: Resources? = context?.getResources() + mNoTitleString = res?.getString(R.string.no_title_label) + mNoColorColor = res?.getColor(R.color.event_center) as Int + // Sort events in two passes so we ensure the allday and standard events + // get sorted in the correct order + cEvents.moveToPosition(-1) + while (cEvents.moveToNext()) { + val e = generateEventFromCursor(cEvents) + if (e.startDay > endDay || e.endDay < startDay) { + continue + } + events.add(e) + } + } + + /** + * @param cEvents Cursor pointing at event + * @return An event created from the cursor + */ + @JvmStatic private fun generateEventFromCursor(cEvents: Cursor): Event { + val e = Event() + e.id = cEvents.getLong(PROJECTION_EVENT_ID_INDEX) + e.title = cEvents.getString(PROJECTION_TITLE_INDEX) + e.location = cEvents.getString(PROJECTION_LOCATION_INDEX) + e.allDay = cEvents.getInt(PROJECTION_ALL_DAY_INDEX) !== 0 + e.organizer = cEvents.getString(PROJECTION_ORGANIZER_INDEX) + e.guestsCanModify = cEvents.getInt(PROJECTION_GUESTS_CAN_INVITE_OTHERS_INDEX) !== 0 + if (e.title == null || e.title!!.length == 0) { + e.title = mNoTitleString + } + if (!cEvents.isNull(PROJECTION_COLOR_INDEX)) { + // Read the color from the database + e.color = Utils.getDisplayColorFromColor(cEvents.getInt(PROJECTION_COLOR_INDEX)) + } else { + e.color = mNoColorColor + } + val eStart: Long = cEvents.getLong(PROJECTION_BEGIN_INDEX) + val eEnd: Long = cEvents.getLong(PROJECTION_END_INDEX) + e.startMillis = eStart + e.startTime = cEvents.getInt(PROJECTION_START_MINUTE_INDEX) + e.startDay = cEvents.getInt(PROJECTION_START_DAY_INDEX) + e.endMillis = eEnd + e.endTime = cEvents.getInt(PROJECTION_END_MINUTE_INDEX) + e.endDay = cEvents.getInt(PROJECTION_END_DAY_INDEX) + e.hasAlarm = cEvents.getInt(PROJECTION_HAS_ALARM_INDEX) !== 0 + + // Check if this is a repeating event + val rrule: String = cEvents.getString(PROJECTION_RRULE_INDEX) + val rdate: String = cEvents.getString(PROJECTION_RDATE_INDEX) + if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)) { + e.isRepeating = true + } else { + e.isRepeating = false + } + e.selfAttendeeStatus = cEvents.getInt(PROJECTION_SELF_ATTENDEE_STATUS_INDEX) + return e + } + + /** + * Computes a position for each event. Each event is displayed + * as a non-overlapping rectangle. For normal events, these rectangles + * are displayed in separate columns in the week view and day view. For + * all-day events, these rectangles are displayed in separate rows along + * the top. In both cases, each event is assigned two numbers: N, and + * Max, that specify that this event is the Nth event of Max number of + * events that are displayed in a group. The width and position of each + * rectangle depend on the maximum number of rectangles that occur at + * the same time. + * + * @param eventsList the list of events, sorted into increasing time order + * @param minimumDurationMillis minimum duration acceptable as cell height of each event + * rectangle in millisecond. Should be 0 when it is not determined. + */ + /* package */ + @JvmStatic fun computePositions( + eventsList: ArrayList<Event>?, + minimumDurationMillis: Long + ) { + if (eventsList == null) { + return + } + + // Compute the column positions separately for the all-day events + doComputePositions(eventsList, minimumDurationMillis, false) + doComputePositions(eventsList, minimumDurationMillis, true) + } + + @JvmStatic private fun doComputePositions( + eventsList: ArrayList<Event>, + minimumDurationMillis: Long, + doAlldayEvents: Boolean + ) { + var minimumDurationMillis = minimumDurationMillis + val activeList: ArrayList<Event> = ArrayList<Event>() + val groupList: ArrayList<Event> = ArrayList<Event>() + if (minimumDurationMillis < 0) { + minimumDurationMillis = 0 + } + var colMask: Long = 0 + var maxCols = 0 + for (event in eventsList) { + // Process all-day events separately + if (event.drawAsAllday() != doAlldayEvents) continue + colMask = if (!doAlldayEvents) { + removeNonAlldayActiveEvents( + event, activeList.iterator() as Iterator<Event>, + minimumDurationMillis, colMask + ) + } else { + removeAlldayActiveEvents(event, activeList.iterator() + as Iterator<Event>, colMask) + } + + // If the active list is empty, then reset the max columns, clear + // the column bit mask, and empty the groupList. + if (activeList.isEmpty()) { + for (ev in groupList) { + ev.maxColumns = maxCols + } + maxCols = 0 + colMask = 0 + groupList.clear() + } + + // Find the first empty column. Empty columns are represented by + // zero bits in the column mask "colMask". + var col = findFirstZeroBit(colMask) + if (col == 64) col = 63 + colMask = colMask or (1L shl col) + event.column = col + activeList.add(event) + groupList.add(event) + val len: Int = activeList.size + if (maxCols < len) maxCols = len + } + for (ev in groupList) { + ev.maxColumns = maxCols + } + } + + @JvmStatic private fun removeAlldayActiveEvents( + event: Event, + iter: Iterator<Event>, + colMask: Long + ): Long { + // Remove the inactive allday events. An event on the active list + // becomes inactive when the end day is less than the current event's + // start day. + var colMask = colMask + while (iter.hasNext()) { + val active = iter.next() + if (active.endDay < event.startDay) { + colMask = colMask and (1L shl active.column).inv() + iter.remove() + } + } + return colMask + } + + @JvmStatic private fun removeNonAlldayActiveEvents( + event: Event, + iter: Iterator<Event>, + minDurationMillis: Long, + colMask: Long + ): Long { + var colMask = colMask + val start = event.getStartMillis() + // Remove the inactive events. An event on the active list + // becomes inactive when its end time is less than or equal to + // the current event's start time. + while (iter.hasNext()) { + val active = iter.next() + val duration: Long = Math.max( + active.getEndMillis() - active.getStartMillis(), minDurationMillis + ) + if (active.getStartMillis() + duration <= start) { + colMask = colMask and (1L shl active.column).inv() + iter.remove() + } + } + return colMask + } + + @JvmStatic fun findFirstZeroBit(`val`: Long): Int { + for (ii in 0..63) { + if (`val` and (1L shl ii) == 0L) return ii + } + return 64 + } + + init { + if (!Utils.isJellybeanOrLater()) { + EVENT_PROJECTION[PROJECTION_COLOR_INDEX] = Instances.CALENDAR_COLOR + } + } + } + + @JvmField var id: Long = 0 + @JvmField var color = 0 + @JvmField var title: CharSequence? = null + @JvmField var location: CharSequence? = null + @JvmField var allDay = false + @JvmField var organizer: String? = null + @JvmField var guestsCanModify = false + @JvmField var startDay = 0 // start Julian day + @JvmField var endDay = 0 // end Julian day + @JvmField var startTime = 0 // Start and end time are in minutes since midnight + @JvmField var endTime = 0 + @JvmField var startMillis = 0L // UTC milliseconds since the epoch + @JvmField var endMillis = 0L // UTC milliseconds since the epoch + @JvmField var column = 0 + @JvmField var maxColumns = 0 + @JvmField var hasAlarm = false + @JvmField var isRepeating = false + @JvmField var selfAttendeeStatus = 0 + + // The coordinates of the event rectangle drawn on the screen. + @JvmField var left = 0f + @JvmField var right = 0f + @JvmField var top = 0f + @JvmField var bottom = 0f + + // These 4 fields are used for navigating among events within the selected + // hour in the Day and Week view. + @JvmField var nextRight: Event? = null + @JvmField var nextLeft: Event? = null + @JvmField var nextUp: Event? = null + @JvmField var nextDown: Event? = null + @Override + @Throws(CloneNotSupportedException::class) + override fun clone(): Object { + super.clone() + val e = Event() + e.title = title + e.color = color + e.location = location + e.allDay = allDay + e.startDay = startDay + e.endDay = endDay + e.startTime = startTime + e.endTime = endTime + e.startMillis = startMillis + e.endMillis = endMillis + e.hasAlarm = hasAlarm + e.isRepeating = isRepeating + e.selfAttendeeStatus = selfAttendeeStatus + e.organizer = organizer + e.guestsCanModify = guestsCanModify + return e as Object + } + + fun copyTo(dest: Event) { + dest.id = id + dest.title = title + dest.color = color + dest.location = location + dest.allDay = allDay + dest.startDay = startDay + dest.endDay = endDay + dest.startTime = startTime + dest.endTime = endTime + dest.startMillis = startMillis + dest.endMillis = endMillis + dest.hasAlarm = hasAlarm + dest.isRepeating = isRepeating + dest.selfAttendeeStatus = selfAttendeeStatus + dest.organizer = organizer + dest.guestsCanModify = guestsCanModify + } + + fun dump() { + Log.e("Cal", "+-----------------------------------------+") + Log.e("Cal", "+ id = $id") + Log.e("Cal", "+ color = $color") + Log.e("Cal", "+ title = $title") + Log.e("Cal", "+ location = $location") + Log.e("Cal", "+ allDay = $allDay") + Log.e("Cal", "+ startDay = $startDay") + Log.e("Cal", "+ endDay = $endDay") + Log.e("Cal", "+ startTime = $startTime") + Log.e("Cal", "+ endTime = $endTime") + Log.e("Cal", "+ organizer = $organizer") + Log.e("Cal", "+ guestwrt = $guestsCanModify") + } + + fun intersects( + julianDay: Int, + startMinute: Int, + endMinute: Int + ): Boolean { + if (endDay < julianDay) { + return false + } + if (startDay > julianDay) { + return false + } + if (endDay == julianDay) { + if (endTime < startMinute) { + return false + } + // An event that ends at the start minute should not be considered + // as intersecting the given time span, but don't exclude + // zero-length (or very short) events. + if (endTime == startMinute && + (startTime != endTime || startDay != endDay)) { + return false + } + } + return !(startDay == julianDay && startTime > endMinute) + } + + /** + * Returns the event title and location separated by a comma. If the + * location is already part of the title (at the end of the title), then + * just the title is returned. + * + * @return the event title and location as a String + */ + val titleAndLocation: String + get() { + var text = title.toString() + + // Append the location to the title, unless the title ends with the + // location (for example, "meeting in building 42" ends with the + // location). + if (location != null) { + val locationString = location.toString() + if (!text.endsWith(locationString)) { + text += ", $locationString" + } + } + return text + } + + // TODO(damianpatel): this getter will likely not be + // needed once DayView.java is converted + fun getColumn(): Int { + return column + } + + fun setStartMillis(startMillis: Long) { + this.startMillis = startMillis + } + + fun getStartMillis(): Long { + return startMillis + } + + fun setEndMillis(endMillis: Long) { + this.endMillis = endMillis + } + + fun getEndMillis(): Long { + return endMillis + } + + fun drawAsAllday(): Boolean { + // Use >= so we'll pick up Exchange allday events + return allDay || endMillis - startMillis >= DateUtils.DAY_IN_MILLIS + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/EventGeometry.java b/src/com/android/calendar/EventGeometry.java deleted file mode 100644 index cdecb49c..00000000 --- a/src/com/android/calendar/EventGeometry.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.calendar; - -import android.graphics.Rect; - -public class EventGeometry { - // This is the space from the grid line to the event rectangle. - private int mCellMargin = 0; - - private float mMinuteHeight; - - private float mHourGap; - private float mMinEventHeight; - - void setCellMargin(int cellMargin) { - mCellMargin = cellMargin; - } - - public void setHourGap(float gap) { - mHourGap = gap; - } - - public void setMinEventHeight(float height) { - mMinEventHeight = height; - } - - public void setHourHeight(float height) { - mMinuteHeight = height / 60.0f; - } - - // Computes the rectangle coordinates of the given event on the screen. - // Returns true if the rectangle is visible on the screen. - public boolean computeEventRect(int date, int left, int top, int cellWidth, Event event) { - if (event.drawAsAllday()) { - return false; - } - - float cellMinuteHeight = mMinuteHeight; - int startDay = event.startDay; - int endDay = event.endDay; - - if (startDay > date || endDay < date) { - return false; - } - - int startTime = event.startTime; - int endTime = event.endTime; - - // If the event started on a previous day, then show it starting - // at the beginning of this day. - if (startDay < date) { - startTime = 0; - } - - // If the event ends on a future day, then show it extending to - // the end of this day. - if (endDay > date) { - endTime = DayView.MINUTES_PER_DAY; - } - - int col = event.getColumn(); - int maxCols = event.getMaxColumns(); - int startHour = startTime / 60; - int endHour = endTime / 60; - - // If the end point aligns on a cell boundary then count it as - // ending in the previous cell so that we don't cross the border - // between hours. - if (endHour * 60 == endTime) - endHour -= 1; - - event.top = top; - event.top += (int) (startTime * cellMinuteHeight); - event.top += startHour * mHourGap; - - event.bottom = top; - event.bottom += (int) (endTime * cellMinuteHeight); - event.bottom += endHour * mHourGap - 1; - - // Make the rectangle be at least mMinEventHeight pixels high - if (event.bottom < event.top + mMinEventHeight) { - event.bottom = event.top + mMinEventHeight; - } - - float colWidth = (float) (cellWidth - (maxCols + 1) * mCellMargin) / (float) maxCols; - event.left = left + col * (colWidth + mCellMargin); - event.right = event.left + colWidth; - return true; - } - - /** - * Returns true if this event intersects the selection region. - */ - boolean eventIntersectsSelection(Event event, Rect selection) { - if (event.left < selection.right && event.right >= selection.left - && event.top < selection.bottom && event.bottom >= selection.top) { - return true; - } - return false; - } - - /** - * Computes the distance from the given point to the given event. - */ - float pointToEvent(float x, float y, Event event) { - float left = event.left; - float right = event.right; - float top = event.top; - float bottom = event.bottom; - - if (x >= left) { - if (x <= right) { - if (y >= top) { - if (y <= bottom) { - // x,y is inside the event rectangle - return 0f; - } - // x,y is below the event rectangle - return y - bottom; - } - // x,y is above the event rectangle - return top - y; - } - - // x > right - float dx = x - right; - if (y < top) { - // the upper right corner - float dy = top - y; - return (float) Math.sqrt(dx * dx + dy * dy); - } - if (y > bottom) { - // the lower right corner - float dy = y - bottom; - return (float) Math.sqrt(dx * dx + dy * dy); - } - // x,y is to the right of the event rectangle - return dx; - } - // x < left - float dx = left - x; - if (y < top) { - // the upper left corner - float dy = top - y; - return (float) Math.sqrt(dx * dx + dy * dy); - } - if (y > bottom) { - // the lower left corner - float dy = y - bottom; - return (float) Math.sqrt(dx * dx + dy * dy); - } - // x,y is to the left of the event rectangle - return dx; - } -} diff --git a/src/com/android/calendar/EventGeometry.kt b/src/com/android/calendar/EventGeometry.kt new file mode 100644 index 00000000..43fc3e77 --- /dev/null +++ b/src/com/android/calendar/EventGeometry.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.graphics.Rect + +class EventGeometry { + // This is the space from the grid line to the event rectangle. + private var mCellMargin = 0 + private var mMinuteHeight = 0f + private var mHourGap = 0f + private var mMinEventHeight = 0f + fun setCellMargin(cellMargin: Int) { + mCellMargin = cellMargin + } + + fun setHourGap(gap: Float) { + mHourGap = gap + } + + fun setMinEventHeight(height: Float) { + mMinEventHeight = height + } + + fun setHourHeight(height: Float) { + mMinuteHeight = height / 60.0f + } + + // Computes the rectangle coordinates of the given event on the screen. + // Returns true if the rectangle is visible on the screen. + fun computeEventRect(date: Int, left: Int, top: Int, cellWidth: Int, event: Event): Boolean { + if (event.drawAsAllday()) { + return false + } + val cellMinuteHeight = mMinuteHeight + val startDay: Int = event.startDay + val endDay: Int = event.endDay + if (startDay > date || endDay < date) { + return false + } + var startTime: Int = event.startTime + var endTime: Int = event.endTime + + // If the event started on a previous day, then show it starting + // at the beginning of this day. + if (startDay < date) { + startTime = 0 + } + + // If the event ends on a future day, then show it extending to + // the end of this day. + if (endDay > date) { + endTime = DayView.MINUTES_PER_DAY + } + val col: Int = event.column + val maxCols: Int = event.maxColumns + val startHour = startTime / 60 + var endHour = endTime / 60 + + // If the end point aligns on a cell boundary then count it as + // ending in the previous cell so that we don't cross the border + // between hours. + if (endHour * 60 == endTime) endHour -= 1 + event.top = top as Float + event.top += (startTime * cellMinuteHeight).toInt() + event.top += startHour * mHourGap + event.bottom = top as Float + event.bottom += (endTime * cellMinuteHeight).toInt() + event.bottom += endHour * mHourGap - 1 + + // Make the rectangle be at least mMinEventHeight pixels high + if (event.bottom < event.top + mMinEventHeight) { + event.bottom = event.top + mMinEventHeight + } + val colWidth = (cellWidth - (maxCols + 1) * mCellMargin).toFloat() / maxCols.toFloat() + event.left = left + col * (colWidth + mCellMargin) + event.right = event.left + colWidth + return true + } + + /** + * Returns true if this event intersects the selection region. + */ + fun eventIntersectsSelection(event: Event, selection: Rect): Boolean { + return if (event.left < selection.right && event.right >= selection.left && + event.top < selection.bottom && event.bottom >= selection.top) { + true + } else false + } + + /** + * Computes the distance from the given point to the given event. + */ + fun pointToEvent(x: Float, y: Float, event: Event): Float { + val left: Float = event.left + val right: Float = event.right + val top: Float = event.top + val bottom: Float = event.bottom + if (x >= left) { + if (x <= right) { + return if (y >= top) { + if (y <= bottom) { + // x,y is inside the event rectangle + 0f + } else y - bottom + // x,y is below the event rectangle + } else top - y + // x,y is above the event rectangle + } + + // x > right + val dx = x - right + if (y < top) { + // the upper right corner + val dy = top - y + return (Math.sqrt(dx as Double * dx + dy as Double * dy)) as Float + } + if (y > bottom) { + // the lower right corner + val dy = y - bottom + return (Math.sqrt(dx as Double * dx + dy as Double * dy)) as Float + } + // x,y is to the right of the event rectangle + return dx + } + // x < left + val dx = left - x + if (y < top) { + // the upper left corner + val dy = top - y + return (Math.sqrt(dx as Double * dx + dy as Double * dy)) as Float + } + if (y > bottom) { + // the lower left corner + val dy = y - bottom + return (Math.sqrt(dx as Double * dx + dy as Double * dy)) as Float + } + // x,y is to the left of the event rectangle + return dx + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/EventInfoActivity.java b/src/com/android/calendar/EventInfoActivity.java deleted file mode 100644 index 626e099d..00000000 --- a/src/com/android/calendar/EventInfoActivity.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar; - -import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; -import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; -import static android.provider.CalendarContract.Attendees.ATTENDEE_STATUS; - -import android.app.ActionBar; -import android.app.Activity; -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.content.Intent; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.provider.CalendarContract; -import android.provider.CalendarContract.Attendees; -import android.util.Log; -import android.widget.Toast; - -import java.util.ArrayList; -import java.util.List; - -public class EventInfoActivity extends Activity { - private static final String TAG = "EventInfoActivity"; - private EventInfoFragment mInfoFragment; - private long mStartMillis, mEndMillis; - private long mEventId; - - // Create an observer so that we can update the views whenever a - // Calendar event changes. - private final ContentObserver mObserver = new ContentObserver(new Handler()) { - @Override - public boolean deliverSelfNotifications() { - return false; - } - - @Override - public void onChange(boolean selfChange) { - if (selfChange) return; - if (mInfoFragment != null) { - mInfoFragment.reloadEvents(); - } - } - }; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - // Get the info needed for the fragment - Intent intent = getIntent(); - int attendeeResponse = 0; - mEventId = -1; - boolean isDialog = false; - - if (icicle != null) { - mEventId = icicle.getLong(EventInfoFragment.BUNDLE_KEY_EVENT_ID); - mStartMillis = icicle.getLong(EventInfoFragment.BUNDLE_KEY_START_MILLIS); - mEndMillis = icicle.getLong(EventInfoFragment.BUNDLE_KEY_END_MILLIS); - attendeeResponse = icicle.getInt(EventInfoFragment.BUNDLE_KEY_ATTENDEE_RESPONSE); - isDialog = icicle.getBoolean(EventInfoFragment.BUNDLE_KEY_IS_DIALOG); - } else if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) { - mStartMillis = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, 0); - mEndMillis = intent.getLongExtra(EXTRA_EVENT_END_TIME, 0); - attendeeResponse = intent.getIntExtra(ATTENDEE_STATUS, - Attendees.ATTENDEE_STATUS_NONE); - Uri data = intent.getData(); - if (data != null) { - try { - List<String> pathSegments = data.getPathSegments(); - int size = pathSegments.size(); - if (size > 2 && "EventTime".equals(pathSegments.get(2))) { - // Support non-standard VIEW intent format: - //dat = content://com.android.calendar/events/[id]/EventTime/[start]/[end] - mEventId = Long.parseLong(pathSegments.get(1)); - if (size > 4) { - mStartMillis = Long.parseLong(pathSegments.get(3)); - mEndMillis = Long.parseLong(pathSegments.get(4)); - } - } else { - mEventId = Long.parseLong(data.getLastPathSegment()); - } - } catch (NumberFormatException e) { - if (mEventId == -1) { - // do nothing here , deal with it later - } else if (mStartMillis == 0 || mEndMillis ==0) { - // Parsing failed on the start or end time , make sure the times were not - // pulled from the intent's extras and reset them. - mStartMillis = 0; - mEndMillis = 0; - } - } - } - } - - if (mEventId == -1) { - Log.w(TAG, "No event id"); - Toast.makeText(this, R.string.event_not_found, Toast.LENGTH_SHORT).show(); - finish(); - } - - // If we do not support showing full screen event info in this configuration, - // close the activity and show the event in AllInOne. - Resources res = getResources(); - if (!res.getBoolean(R.bool.agenda_show_event_info_full_screen) - && !res.getBoolean(R.bool.show_event_info_full_screen)) { - CalendarController.getInstance(this) - .launchViewEvent(mEventId, mStartMillis, mEndMillis, attendeeResponse); - finish(); - return; - } - - setContentView(R.layout.simple_frame_layout); - - // Get the fragment if exists - mInfoFragment = (EventInfoFragment) - getFragmentManager().findFragmentById(R.id.main_frame); - - - // Remove the application title - ActionBar bar = getActionBar(); - if (bar != null) { - bar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME); - } - - // Create a new fragment if none exists - if (mInfoFragment == null) { - FragmentManager fragmentManager = getFragmentManager(); - FragmentTransaction ft = fragmentManager.beginTransaction(); - mInfoFragment = new EventInfoFragment(this, mEventId, mStartMillis, mEndMillis, - attendeeResponse, isDialog, (isDialog ? - EventInfoFragment.DIALOG_WINDOW_STYLE : - EventInfoFragment.FULL_WINDOW_STYLE)); - ft.replace(R.id.main_frame, mInfoFragment); - ft.commit(); - } - } - - @Override - protected void onNewIntent(Intent intent) { - // From the Android Dev Guide: "It's important to note that when - // onNewIntent(Intent) is called, the Activity has not been restarted, - // so the getIntent() method will still return the Intent that was first - // received with onCreate(). This is why setIntent(Intent) is called - // inside onNewIntent(Intent) (just in case you call getIntent() at a - // later time)." - setIntent(intent); - } - - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - protected void onResume() { - super.onResume(); - getContentResolver().registerContentObserver(CalendarContract.Events.CONTENT_URI, - true, mObserver); - } - - @Override - protected void onPause() { - super.onPause(); - getContentResolver().unregisterContentObserver(mObserver); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - } -} diff --git a/src/com/android/calendar/EventInfoActivity.kt b/src/com/android/calendar/EventInfoActivity.kt new file mode 100644 index 00000000..c0a1b9cd --- /dev/null +++ b/src/com/android/calendar/EventInfoActivity.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME +import android.provider.CalendarContract.EXTRA_EVENT_END_TIME +import android.provider.CalendarContract.Attendees.ATTENDEE_STATUS +import android.app.ActionBar +import android.app.Activity +import android.app.FragmentManager +import android.app.FragmentTransaction +import android.content.Intent +import android.content.res.Resources +import android.database.ContentObserver +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.provider.CalendarContract +import android.provider.CalendarContract.Attendees +import android.util.Log +import android.widget.Toast + +class EventInfoActivity : Activity() { + private var mInfoFragment: EventInfoFragment? = null + private var mStartMillis: Long = 0 + private var mEndMillis: Long = 0 + private var mEventId: Long = 0 + + // Create an observer so that we can update the views whenever a + // Calendar event changes. + private val mObserver: ContentObserver = object : ContentObserver(Handler()) { + @Override + override fun deliverSelfNotifications(): Boolean { + return false + } + + @Override + override fun onChange(selfChange: Boolean) { + if (selfChange) return + val temp = mInfoFragment + if (temp != null) { + mInfoFragment?.reloadEvents() + } + } + } + + @Override + protected override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + + // Get the info needed for the fragment + val intent: Intent = getIntent() + var attendeeResponse = 0 + mEventId = -1 + var isDialog = false + if (icicle != null) { + mEventId = icicle.getLong(EventInfoFragment.BUNDLE_KEY_EVENT_ID) + mStartMillis = icicle.getLong(EventInfoFragment.BUNDLE_KEY_START_MILLIS) + mEndMillis = icicle.getLong(EventInfoFragment.BUNDLE_KEY_END_MILLIS) + attendeeResponse = icicle.getInt(EventInfoFragment.BUNDLE_KEY_ATTENDEE_RESPONSE) + isDialog = icicle.getBoolean(EventInfoFragment.BUNDLE_KEY_IS_DIALOG) + } else if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) { + mStartMillis = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, 0) + mEndMillis = intent.getLongExtra(EXTRA_EVENT_END_TIME, 0) + attendeeResponse = intent.getIntExtra( + ATTENDEE_STATUS, + Attendees.ATTENDEE_STATUS_NONE + ) + val data: Uri? = intent.getData() + if (data != null) { + try { + val pathSegments = data.getPathSegments() + val size: Int = pathSegments.size + if (size > 2 && "EventTime".equals(pathSegments[2])) { + // Support non-standard VIEW intent format: + // dat = content://com.android.calendar/events/[id]/EventTime/[start]/[end] + mEventId = pathSegments[1].toLong() + if (size > 4) { + mStartMillis = pathSegments[3].toLong() + mEndMillis = pathSegments[4].toLong() + } + } else { + mEventId = data.getLastPathSegment() as Long + } + } catch (e: NumberFormatException) { + if (mEventId == -1L) { + // do nothing here , deal with it later + } else if (mStartMillis == 0L || mEndMillis == 0L) { + // Parsing failed on the start or end time , make sure the times were not + // pulled from the intent's extras and reset them. + mStartMillis = 0 + mEndMillis = 0 + } + } + } + } + if (mEventId == -1L) { + Log.w(TAG, "No event id") + Toast.makeText(this, R.string.event_not_found, Toast.LENGTH_SHORT).show() + finish() + } + + // If we do not support showing full screen event info in this configuration, + // close the activity and show the event in AllInOne. + val res: Resources = getResources() + if (!res.getBoolean(R.bool.agenda_show_event_info_full_screen) && + !res.getBoolean(R.bool.show_event_info_full_screen) + ) { + CalendarController.getInstance(this) + ?.launchViewEvent(mEventId, mStartMillis, mEndMillis, attendeeResponse) + finish() + return + } + setContentView(R.layout.simple_frame_layout) + + // Get the fragment if exists + mInfoFragment = getFragmentManager().findFragmentById(R.id.main_frame) as EventInfoFragment + + // Remove the application title + val bar: ActionBar? = getActionBar() + if (bar != null) { + bar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP or ActionBar.DISPLAY_SHOW_HOME) + } + + // Create a new fragment if none exists + if (mInfoFragment == null) { + val fragmentManager: FragmentManager = getFragmentManager() + val ft: FragmentTransaction = fragmentManager.beginTransaction() + mInfoFragment = EventInfoFragment( + this, + mEventId, + mStartMillis, + mEndMillis, + attendeeResponse, + isDialog, + if (isDialog) EventInfoFragment.DIALOG_WINDOW_STYLE + else EventInfoFragment.FULL_WINDOW_STYLE + ) + ft.replace(R.id.main_frame, mInfoFragment) + ft.commit() + } + } + + @Override + protected override fun onNewIntent(intent: Intent?) { + // From the Android Dev Guide: "It's important to note that when + // onNewIntent(Intent) is called, the Activity has not been restarted, + // so the getIntent() method will still return the Intent that was first + // received with onCreate(). This is why setIntent(Intent) is called + // inside onNewIntent(Intent) (just in case you call getIntent() at a + // later time)." + setIntent(intent) + } + + @Override + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + } + + @Override + protected override fun onResume() { + super.onResume() + getContentResolver().registerContentObserver( + CalendarContract.Events.CONTENT_URI, + true, mObserver + ) + } + + @Override + protected override fun onPause() { + super.onPause() + getContentResolver().unregisterContentObserver(mObserver) + } + + @Override + protected override fun onDestroy() { + super.onDestroy() + } + + companion object { + private const val TAG = "EventInfoActivity" + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/EventInfoFragment.java b/src/com/android/calendar/EventInfoFragment.java deleted file mode 100644 index 0aa83d02..00000000 --- a/src/com/android/calendar/EventInfoFragment.java +++ /dev/null @@ -1,877 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar; - -import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY; -import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; -import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; -import static com.android.calendar.CalendarController.EVENT_EDIT_ON_LAUNCH; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.app.Activity; -import android.app.Dialog; -import android.app.DialogFragment; -import android.app.FragmentManager; -import android.app.Service; -import android.content.ActivityNotFoundException; -import android.content.ContentProviderOperation; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.provider.CalendarContract; -import android.provider.CalendarContract.Attendees; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Events; -import android.provider.CalendarContract.Reminders; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds; -import android.provider.ContactsContract.Intents; -import android.provider.ContactsContract.QuickContact; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.format.Time; -import android.text.method.LinkMovementMethod; -import android.text.method.MovementMethod; -import android.text.style.ForegroundColorSpan; -import android.text.util.Rfc822Token; -import android.util.Log; -import android.util.SparseIntArray; -import android.view.Gravity; -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.View.OnClickListener; -import android.view.View.OnTouchListener; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.RadioGroup.OnCheckedChangeListener; -import android.widget.ScrollView; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.calendar.CalendarController.EventInfo; -import com.android.calendar.CalendarController.EventType; -import com.android.calendar.alerts.QuickResponseActivity; -import com.android.calendarcommon2.DateException; -import com.android.calendarcommon2.Duration; -import com.android.calendarcommon2.EventRecurrence; -import com.android.colorpicker.HsvColorComparator; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener, - CalendarController.EventHandler, OnClickListener { - - public static final boolean DEBUG = false; - - public static final String TAG = "EventInfoFragment"; - - protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id"; - protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis"; - protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis"; - protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog"; - protected static final String BUNDLE_KEY_DELETE_DIALOG_VISIBLE = "key_delete_dialog_visible"; - protected static final String BUNDLE_KEY_WINDOW_STYLE = "key_window_style"; - protected static final String BUNDLE_KEY_CALENDAR_COLOR = "key_calendar_color"; - protected static final String BUNDLE_KEY_CALENDAR_COLOR_INIT = "key_calendar_color_init"; - protected static final String BUNDLE_KEY_CURRENT_COLOR = "key_current_color"; - protected static final String BUNDLE_KEY_CURRENT_COLOR_KEY = "key_current_color_key"; - protected static final String BUNDLE_KEY_CURRENT_COLOR_INIT = "key_current_color_init"; - protected static final String BUNDLE_KEY_ORIGINAL_COLOR = "key_original_color"; - protected static final String BUNDLE_KEY_ORIGINAL_COLOR_INIT = "key_original_color_init"; - protected static final String BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response"; - protected static final String BUNDLE_KEY_USER_SET_ATTENDEE_RESPONSE = - "key_user_set_attendee_response"; - protected static final String BUNDLE_KEY_TENTATIVE_USER_RESPONSE = - "key_tentative_user_response"; - protected static final String BUNDLE_KEY_RESPONSE_WHICH_EVENTS = "key_response_which_events"; - protected static final String BUNDLE_KEY_REMINDER_MINUTES = "key_reminder_minutes"; - protected static final String BUNDLE_KEY_REMINDER_METHODS = "key_reminder_methods"; - - - private static final String PERIOD_SPACE = ". "; - - private static final String NO_EVENT_COLOR = ""; - - /** - * These are the corresponding indices into the array of strings - * "R.array.change_response_labels" in the resource file. - */ - static final int UPDATE_SINGLE = 0; - static final int UPDATE_ALL = 1; - - // Style of view - public static final int FULL_WINDOW_STYLE = 0; - public static final int DIALOG_WINDOW_STYLE = 1; - - private int mWindowStyle = DIALOG_WINDOW_STYLE; - - // Query tokens for QueryHandler - private static final int TOKEN_QUERY_EVENT = 1 << 0; - private static final int TOKEN_QUERY_CALENDARS = 1 << 1; - private static final int TOKEN_QUERY_ATTENDEES = 1 << 2; - private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3; - private static final int TOKEN_QUERY_REMINDERS = 1 << 4; - private static final int TOKEN_QUERY_VISIBLE_CALENDARS = 1 << 5; - private static final int TOKEN_QUERY_COLORS = 1 << 6; - - private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS - | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT - | TOKEN_QUERY_REMINDERS | TOKEN_QUERY_VISIBLE_CALENDARS | TOKEN_QUERY_COLORS; - - private int mCurrentQuery = 0; - - private static final String[] EVENT_PROJECTION = new String[] { - Events._ID, // 0 do not remove; used in DeleteEventHelper - Events.TITLE, // 1 do not remove; used in DeleteEventHelper - Events.RRULE, // 2 do not remove; used in DeleteEventHelper - Events.ALL_DAY, // 3 do not remove; used in DeleteEventHelper - Events.CALENDAR_ID, // 4 do not remove; used in DeleteEventHelper - Events.DTSTART, // 5 do not remove; used in DeleteEventHelper - Events._SYNC_ID, // 6 do not remove; used in DeleteEventHelper - Events.EVENT_TIMEZONE, // 7 do not remove; used in DeleteEventHelper - Events.DESCRIPTION, // 8 - Events.EVENT_LOCATION, // 9 - Calendars.CALENDAR_ACCESS_LEVEL, // 10 - Events.CALENDAR_COLOR, // 11 - Events.EVENT_COLOR, // 12 - Events.HAS_ATTENDEE_DATA, // 13 - Events.ORGANIZER, // 14 - Events.HAS_ALARM, // 15 - Calendars.MAX_REMINDERS, // 16 - Calendars.ALLOWED_REMINDERS, // 17 - Events.CUSTOM_APP_PACKAGE, // 18 - Events.CUSTOM_APP_URI, // 19 - Events.DTEND, // 20 - Events.DURATION, // 21 - Events.ORIGINAL_SYNC_ID // 22 do not remove; used in DeleteEventHelper - }; - private static final int EVENT_INDEX_ID = 0; - private static final int EVENT_INDEX_TITLE = 1; - private static final int EVENT_INDEX_RRULE = 2; - private static final int EVENT_INDEX_ALL_DAY = 3; - private static final int EVENT_INDEX_CALENDAR_ID = 4; - private static final int EVENT_INDEX_DTSTART = 5; - private static final int EVENT_INDEX_SYNC_ID = 6; - private static final int EVENT_INDEX_EVENT_TIMEZONE = 7; - private static final int EVENT_INDEX_DESCRIPTION = 8; - private static final int EVENT_INDEX_EVENT_LOCATION = 9; - private static final int EVENT_INDEX_ACCESS_LEVEL = 10; - private static final int EVENT_INDEX_CALENDAR_COLOR = 11; - private static final int EVENT_INDEX_EVENT_COLOR = 12; - private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 13; - private static final int EVENT_INDEX_ORGANIZER = 14; - private static final int EVENT_INDEX_HAS_ALARM = 15; - private static final int EVENT_INDEX_MAX_REMINDERS = 16; - private static final int EVENT_INDEX_ALLOWED_REMINDERS = 17; - private static final int EVENT_INDEX_CUSTOM_APP_PACKAGE = 18; - private static final int EVENT_INDEX_CUSTOM_APP_URI = 19; - private static final int EVENT_INDEX_DTEND = 20; - private static final int EVENT_INDEX_DURATION = 21; - - static { - if (!Utils.isJellybeanOrLater()) { - EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_PACKAGE] = Events._ID; // nonessential value - EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_URI] = Events._ID; // nonessential value - } - } - - static final String[] CALENDARS_PROJECTION = new String[] { - Calendars._ID, // 0 - Calendars.CALENDAR_DISPLAY_NAME, // 1 - Calendars.OWNER_ACCOUNT, // 2 - Calendars.CAN_ORGANIZER_RESPOND, // 3 - Calendars.ACCOUNT_NAME, // 4 - Calendars.ACCOUNT_TYPE // 5 - }; - static final int CALENDARS_INDEX_DISPLAY_NAME = 1; - static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; - static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3; - static final int CALENDARS_INDEX_ACCOUNT_NAME = 4; - static final int CALENDARS_INDEX_ACCOUNT_TYPE = 5; - - static final String CALENDARS_WHERE = Calendars._ID + "=?"; - static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?"; - static final String CALENDARS_VISIBLE_WHERE = Calendars.VISIBLE + "=?"; - - public static final int COLORS_INDEX_COLOR = 1; - public static final int COLORS_INDEX_COLOR_KEY = 2; - - private View mView; - - private Uri mUri; - private long mEventId; - private Cursor mEventCursor; - private Cursor mCalendarsCursor; - - private static float mScale = 0; // Used for supporting different screen densities - - private static int mCustomAppIconSize = 32; - - private long mStartMillis; - private long mEndMillis; - private boolean mAllDay; - - private boolean mOwnerCanRespond; - private String mSyncAccountName; - private String mCalendarOwnerAccount; - private boolean mIsBusyFreeCalendar; - - private int mOriginalAttendeeResponse; - private int mAttendeeResponseFromIntent = Attendees.ATTENDEE_STATUS_NONE; - private int mUserSetResponse = Attendees.ATTENDEE_STATUS_NONE; - private int mWhichEvents = -1; - // Used as the temporary response until the dialog is confirmed. It is also - // able to be used as a state marker for configuration changes. - private int mTentativeUserSetResponse = Attendees.ATTENDEE_STATUS_NONE; - private boolean mHasAlarm; - // Used to prevent saving changes in event if it is being deleted. - private boolean mEventDeletionStarted = false; - - private TextView mTitle; - private TextView mWhenDateTime; - private TextView mWhere; - private Menu mMenu = null; - private View mHeadlines; - private ScrollView mScrollView; - private View mLoadingMsgView; - private View mErrorMsgView; - private ObjectAnimator mAnimateAlpha; - private long mLoadingMsgStartTime; - - private SparseIntArray mDisplayColorKeyMap = new SparseIntArray(); - private int mOriginalColor = -1; - private boolean mOriginalColorInitialized = false; - private int mCalendarColor = -1; - private boolean mCalendarColorInitialized = false; - private int mCurrentColor = -1; - private boolean mCurrentColorInitialized = false; - private int mCurrentColorKey = -1; - - private static final int FADE_IN_TIME = 300; // in milliseconds - private static final int LOADING_MSG_DELAY = 600; // in milliseconds - private static final int LOADING_MSG_MIN_DISPLAY_TIME = 600; - private boolean mNoCrossFade = false; // Used to prevent repeated cross-fade - private RadioGroup mResponseRadioGroup; - - ArrayList<String> mToEmails = new ArrayList<String>(); - ArrayList<String> mCcEmails = new ArrayList<String>(); - - - private final Runnable mTZUpdater = new Runnable() { - @Override - public void run() { - updateEvent(mView); - } - }; - - private final Runnable mLoadingMsgAlphaUpdater = new Runnable() { - @Override - public void run() { - // Since this is run after a delay, make sure to only show the message - // if the event's data is not shown yet. - if (!mAnimateAlpha.isRunning() && mScrollView.getAlpha() == 0) { - mLoadingMsgStartTime = System.currentTimeMillis(); - mLoadingMsgView.setAlpha(1); - } - } - }; - - private static int mDialogWidth = 500; - private static int mDialogHeight = 600; - private static int DIALOG_TOP_MARGIN = 8; - private boolean mIsDialog = false; - private boolean mIsPaused = true; - private boolean mDismissOnResume = false; - private int mX = -1; - private int mY = -1; - private int mMinTop; // Dialog cannot be above this location - private boolean mIsTabletConfig; - private Activity mActivity; - private Context mContext; - - private CalendarController mController; - - private void sendAccessibilityEventIfQueryDone(int token) { - mCurrentQuery |= token; - if (mCurrentQuery == TOKEN_QUERY_ALL) { - sendAccessibilityEvent(); - } - } - - public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, - int attendeeResponse, boolean isDialog, int windowStyle) { - - Resources r = context.getResources(); - if (mScale == 0) { - mScale = context.getResources().getDisplayMetrics().density; - if (mScale != 1) { - mCustomAppIconSize *= mScale; - if (isDialog) { - DIALOG_TOP_MARGIN *= mScale; - } - } - } - if (isDialog) { - setDialogSize(r); - } - mIsDialog = isDialog; - - setStyle(DialogFragment.STYLE_NO_TITLE, 0); - mUri = uri; - mStartMillis = startMillis; - mEndMillis = endMillis; - mAttendeeResponseFromIntent = attendeeResponse; - mWindowStyle = windowStyle; - } - - // This is currently required by the fragment manager. - public EventInfoFragment() { - } - - public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, - int attendeeResponse, boolean isDialog, int windowStyle) { - this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis, - endMillis, attendeeResponse, isDialog, windowStyle); - mEventId = eventId; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - if (mIsDialog) { - applyDialogParams(); - } - - final Activity activity = getActivity(); - mContext = activity; - } - - private void applyDialogParams() { - Dialog dialog = getDialog(); - dialog.setCanceledOnTouchOutside(true); - - Window window = dialog.getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); - - WindowManager.LayoutParams a = window.getAttributes(); - a.dimAmount = .4f; - - a.width = mDialogWidth; - a.height = mDialogHeight; - - - // On tablets , do smart positioning of dialog - // On phones , use the whole screen - - if (mX != -1 || mY != -1) { - a.x = mX - mDialogWidth / 2; - a.y = mY - mDialogHeight / 2; - if (a.y < mMinTop) { - a.y = mMinTop + DIALOG_TOP_MARGIN; - } - a.gravity = Gravity.LEFT | Gravity.TOP; - } - window.setAttributes(a); - } - - public void setDialogParams(int x, int y, int minTop) { - mX = x; - mY = y; - mMinTop = minTop; - } - - // Implements OnCheckedChangeListener - @Override - public void onCheckedChanged(RadioGroup group, int checkedId) { - } - - public void onNothingSelected(AdapterView<?> parent) { - } - - @Override - public void onDetach() { - super.onDetach(); - mController.deregisterEventHandler(R.layout.event_info); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - mActivity = activity; - // Ensure that mIsTabletConfig is set before creating the menu. - mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config); - mController = CalendarController.getInstance(mActivity); - mController.registerEventHandler(R.layout.event_info, this); - - if (!mIsDialog) { - setHasOptionsMenu(true); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - if (mWindowStyle == DIALOG_WINDOW_STYLE) { - mView = inflater.inflate(R.layout.event_info_dialog, container, false); - } else { - mView = inflater.inflate(R.layout.event_info, container, false); - } - mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view); - mLoadingMsgView = mView.findViewById(R.id.event_info_loading_msg); - mErrorMsgView = mView.findViewById(R.id.event_info_error_msg); - mTitle = (TextView) mView.findViewById(R.id.title); - mWhenDateTime = (TextView) mView.findViewById(R.id.when_datetime); - mWhere = (TextView) mView.findViewById(R.id.where); - mHeadlines = mView.findViewById(R.id.event_info_headline); - - mResponseRadioGroup = (RadioGroup) mView.findViewById(R.id.response_value); - - mAnimateAlpha = ObjectAnimator.ofFloat(mScrollView, "Alpha", 0, 1); - mAnimateAlpha.setDuration(FADE_IN_TIME); - mAnimateAlpha.addListener(new AnimatorListenerAdapter() { - int defLayerType; - - @Override - public void onAnimationStart(Animator animation) { - // Use hardware layer for better performance during animation - defLayerType = mScrollView.getLayerType(); - mScrollView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - // Ensure that the loading message is gone before showing the - // event info - mLoadingMsgView.removeCallbacks(mLoadingMsgAlphaUpdater); - mLoadingMsgView.setVisibility(View.GONE); - } - - @Override - public void onAnimationCancel(Animator animation) { - mScrollView.setLayerType(defLayerType, null); - } - - @Override - public void onAnimationEnd(Animator animation) { - mScrollView.setLayerType(defLayerType, null); - // Do not cross fade after the first time - mNoCrossFade = true; - } - }); - - mLoadingMsgView.setAlpha(0); - mScrollView.setAlpha(0); - mErrorMsgView.setVisibility(View.INVISIBLE); - mLoadingMsgView.postDelayed(mLoadingMsgAlphaUpdater, LOADING_MSG_DELAY); - - // Hide Edit/Delete buttons if in full screen mode on a phone - if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) { - mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE); - } - - return mView; - } - - private void updateTitle() { - Resources res = getActivity().getResources(); - getActivity().setTitle(res.getString(R.string.event_info_title)); - } - - /** - * Initializes the event cursor, which is expected to point to the first - * (and only) result from a query. - * @return false if the cursor is empty, true otherwise - */ - private boolean initEventCursor() { - if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) { - return false; - } - mEventCursor.moveToFirst(); - mEventId = mEventCursor.getInt(EVENT_INDEX_ID); - String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); - // mHasAlarm will be true if it was saved in the event already. - mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)? true : false; - return true; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - // Show color/edit/delete buttons only in non-dialog configuration - if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) { - inflater.inflate(R.menu.event_info_title_bar, menu); - mMenu = menu; - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - // If we're a dialog we don't want to handle menu buttons - if (mIsDialog) { - return false; - } - // Handles option menu selections: - // Home button - close event info activity and start the main calendar - // one - // Edit button - start the event edit activity and close the info - // activity - // Delete button - start a delete query that calls a runnable that close - // the info activity - - final int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - Utils.returnToCalendarHome(mContext); - mActivity.finish(); - return true; - } else if (itemId == R.id.info_action_edit) { - mActivity.finish(); - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onStop() { - super.onStop(); - } - - @Override - public void onDestroy() { - if (mEventCursor != null) { - mEventCursor.close(); - } - if (mCalendarsCursor != null) { - mCalendarsCursor.close(); - } - super.onDestroy(); - } - - /** - * Creates an exception to a recurring event. The only change we're making is to the - * "self attendee status" value. The provider will take care of updating the corresponding - * Attendees.attendeeStatus entry. - * - * @param eventId The recurring event. - * @param status The new value for selfAttendeeStatus. - */ - private void createExceptionResponse(long eventId, int status) { - ContentValues values = new ContentValues(); - values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis); - values.put(Events.SELF_ATTENDEE_STATUS, status); - values.put(Events.STATUS, Events.STATUS_CONFIRMED); - - ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); - Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI, - String.valueOf(eventId)); - ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build()); - } - - public static int getResponseFromButtonId(int buttonId) { - return Attendees.ATTENDEE_STATUS_NONE; - } - - public static int findButtonIdForResponse(int response) { - return -1; - } - - private void displayEventNotFound() { - mErrorMsgView.setVisibility(View.VISIBLE); - mScrollView.setVisibility(View.GONE); - mLoadingMsgView.setVisibility(View.GONE); - } - - private void updateEvent(View view) { - if (mEventCursor == null || view == null) { - return; - } - - Context context = view.getContext(); - if (context == null) { - return; - } - - String eventName = mEventCursor.getString(EVENT_INDEX_TITLE); - if (eventName == null || eventName.length() == 0) { - eventName = getActivity().getString(R.string.no_title_label); - } - - // 3rd parties might not have specified the start/end time when firing the - // Events.CONTENT_URI intent. Update these with values read from the db. - if (mStartMillis == 0 && mEndMillis == 0) { - mStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART); - mEndMillis = mEventCursor.getLong(EVENT_INDEX_DTEND); - if (mEndMillis == 0) { - String duration = mEventCursor.getString(EVENT_INDEX_DURATION); - if (!TextUtils.isEmpty(duration)) { - try { - Duration d = new Duration(); - d.parse(duration); - long endMillis = mStartMillis + d.getMillis(); - if (endMillis >= mStartMillis) { - mEndMillis = endMillis; - } else { - Log.d(TAG, "Invalid duration string: " + duration); - } - } catch (DateException e) { - Log.d(TAG, "Error parsing duration string " + duration, e); - } - } - if (mEndMillis == 0) { - mEndMillis = mStartMillis; - } - } - } - - mAllDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0; - String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION); - String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION); - String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); - String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); - - mHeadlines.setBackgroundColor(mCurrentColor); - - // What - if (eventName != null) { - setTextCommon(view, R.id.title, eventName); - } - - // When - // Set the date and repeats (if any) - String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater); - - Resources resources = context.getResources(); - String displayedDatetime = Utils.getDisplayedDatetime(mStartMillis, mEndMillis, - System.currentTimeMillis(), localTimezone, mAllDay, context); - - String displayedTimezone = null; - if (!mAllDay) { - displayedTimezone = Utils.getDisplayedTimezone(mStartMillis, localTimezone, - eventTimezone); - } - // Display the datetime. Make the timezone (if any) transparent. - if (displayedTimezone == null) { - setTextCommon(view, R.id.when_datetime, displayedDatetime); - } else { - int timezoneIndex = displayedDatetime.length(); - displayedDatetime += " " + displayedTimezone; - SpannableStringBuilder sb = new SpannableStringBuilder(displayedDatetime); - ForegroundColorSpan transparentColorSpan = new ForegroundColorSpan( - resources.getColor(R.color.event_info_headline_transparent_color)); - sb.setSpan(transparentColorSpan, timezoneIndex, displayedDatetime.length(), - Spannable.SPAN_INCLUSIVE_INCLUSIVE); - setTextCommon(view, R.id.when_datetime, sb); - } - - view.findViewById(R.id.when_repeat).setVisibility(View.GONE); - - // Organizer view is setup in the updateCalendar method - - - // Where - if (location == null || location.trim().length() == 0) { - setVisibilityCommon(view, R.id.where, View.GONE); - } else { - final TextView textView = mWhere; - if (textView != null) { - textView.setText(location.trim()); - } - } - - // Launch Custom App - if (Utils.isJellybeanOrLater()) { - updateCustomAppButton(); - } - } - - private void updateCustomAppButton() { - setVisibilityCommon(mView, R.id.launch_custom_app_container, View.GONE); - return; - } - - private void sendAccessibilityEvent() { - AccessibilityManager am = - (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE); - if (!am.isEnabled()) { - return; - } - - AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); - event.setClassName(EventInfoFragment.class.getName()); - event.setPackageName(getActivity().getPackageName()); - List<CharSequence> text = event.getText(); - - if (mResponseRadioGroup.getVisibility() == View.VISIBLE) { - int id = mResponseRadioGroup.getCheckedRadioButtonId(); - if (id != View.NO_ID) { - text.add(((TextView) getView().findViewById(R.id.response_label)).getText()); - text.add((((RadioButton) (mResponseRadioGroup.findViewById(id))) - .getText() + PERIOD_SPACE)); - } - } - - am.sendAccessibilityEvent(event); - } - - private void updateCalendar(View view) { - - mCalendarOwnerAccount = ""; - if (mCalendarsCursor != null && mEventCursor != null) { - mCalendarsCursor.moveToFirst(); - String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); - mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount; - mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0; - mSyncAccountName = mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME); - - setVisibilityCommon(view, R.id.organizer_container, View.GONE); - mIsBusyFreeCalendar = - mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY; - - if (!mIsBusyFreeCalendar) { - - View b = mView.findViewById(R.id.edit); - b.setEnabled(true); - b.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - // For dialogs, just close the fragment - // For full screen, close activity on phone, leave it for tablet - if (mIsDialog) { - EventInfoFragment.this.dismiss(); - } - else if (!mIsTabletConfig){ - getActivity().finish(); - } - } - }); - } - View button; - if ((!mIsDialog && !mIsTabletConfig || - mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) && mMenu != null) { - mActivity.invalidateOptionsMenu(); - } - } else { - setVisibilityCommon(view, R.id.calendar, View.GONE); - sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS); - } - } - - private void setTextCommon(View view, int id, CharSequence text) { - TextView textView = (TextView) view.findViewById(id); - if (textView == null) - return; - textView.setText(text); - } - - private void setVisibilityCommon(View view, int id, int visibility) { - View v = view.findViewById(id); - if (v != null) { - v.setVisibility(visibility); - } - return; - } - - @Override - public void onPause() { - mIsPaused = true; - super.onPause(); - } - - @Override - public void onResume() { - super.onResume(); - if (mIsDialog) { - setDialogSize(getActivity().getResources()); - applyDialogParams(); - } - mIsPaused = false; - if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) { - int buttonId = findButtonIdForResponse(mTentativeUserSetResponse); - mResponseRadioGroup.check(buttonId); - } - } - - @Override - public void eventsChanged() { - } - - @Override - public long getSupportedEventTypes() { - return EventType.EVENTS_CHANGED; - } - - @Override - public void handleEvent(EventInfo event) { - reloadEvents(); - } - - public void reloadEvents() { - } - - @Override - public void onClick(View view) { - } - - public long getEventId() { - return mEventId; - } - - public long getStartMillis() { - return mStartMillis; - } - public long getEndMillis() { - return mEndMillis; - } - private void setDialogSize(Resources r) { - mDialogWidth = (int)r.getDimension(R.dimen.event_info_dialog_width); - mDialogHeight = (int)r.getDimension(R.dimen.event_info_dialog_height); - } -} diff --git a/src/com/android/calendar/EventInfoFragment.kt b/src/com/android/calendar/EventInfoFragment.kt new file mode 100644 index 00000000..fcc27fc8 --- /dev/null +++ b/src/com/android/calendar/EventInfoFragment.kt @@ -0,0 +1,787 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator +import android.app.Activity +import android.app.Dialog +import android.app.DialogFragment +import android.app.Service +import android.content.ContentProviderOperation +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.content.res.Resources +import android.database.Cursor +import android.net.Uri +import android.os.Bundle +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Events +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.TextUtils +import android.text.style.ForegroundColorSpan +import android.util.Log +import android.util.SparseIntArray +import android.view.Gravity +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.View.OnClickListener +import android.view.ViewGroup +import android.view.Window +import android.view.WindowManager +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityManager +import android.widget.AdapterView +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.RadioGroup.OnCheckedChangeListener +import android.widget.ScrollView +import android.widget.TextView +import com.android.calendar.CalendarController.EventInfo +import com.android.calendar.CalendarController.EventType +import com.android.calendarcommon2.DateException +import com.android.calendarcommon2.Duration +import java.util.ArrayList + +class EventInfoFragment : DialogFragment, OnCheckedChangeListener, CalendarController.EventHandler, + OnClickListener { + private var mWindowStyle = DIALOG_WINDOW_STYLE + private var mCurrentQuery = 0 + + companion object { + const val DEBUG = false + const val TAG = "EventInfoFragment" + internal const val BUNDLE_KEY_EVENT_ID = "key_event_id" + internal const val BUNDLE_KEY_START_MILLIS = "key_start_millis" + internal const val BUNDLE_KEY_END_MILLIS = "key_end_millis" + internal const val BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog" + internal const val BUNDLE_KEY_DELETE_DIALOG_VISIBLE = "key_delete_dialog_visible" + internal const val BUNDLE_KEY_WINDOW_STYLE = "key_window_style" + internal const val BUNDLE_KEY_CALENDAR_COLOR = "key_calendar_color" + internal const val BUNDLE_KEY_CALENDAR_COLOR_INIT = "key_calendar_color_init" + internal const val BUNDLE_KEY_CURRENT_COLOR = "key_current_color" + internal const val BUNDLE_KEY_CURRENT_COLOR_KEY = "key_current_color_key" + internal const val BUNDLE_KEY_CURRENT_COLOR_INIT = "key_current_color_init" + internal const val BUNDLE_KEY_ORIGINAL_COLOR = "key_original_color" + internal const val BUNDLE_KEY_ORIGINAL_COLOR_INIT = "key_original_color_init" + internal const val BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response" + internal const val BUNDLE_KEY_USER_SET_ATTENDEE_RESPONSE = "key_user_set_attendee_response" + internal const val BUNDLE_KEY_TENTATIVE_USER_RESPONSE = "key_tentative_user_response" + internal const val BUNDLE_KEY_RESPONSE_WHICH_EVENTS = "key_response_which_events" + internal const val BUNDLE_KEY_REMINDER_MINUTES = "key_reminder_minutes" + internal const val BUNDLE_KEY_REMINDER_METHODS = "key_reminder_methods" + private const val PERIOD_SPACE = ". " + private const val NO_EVENT_COLOR = "" + + /** + * These are the corresponding indices into the array of strings + * "R.array.change_response_labels" in the resource file. + */ + const val UPDATE_SINGLE = 0 + const val UPDATE_ALL = 1 + + // Style of view + const val FULL_WINDOW_STYLE = 0 + const val DIALOG_WINDOW_STYLE = 1 + + // Query tokens for QueryHandler + private const val TOKEN_QUERY_EVENT = 1 shl 0 + private const val TOKEN_QUERY_CALENDARS = 1 shl 1 + private const val TOKEN_QUERY_ATTENDEES = 1 shl 2 + private const val TOKEN_QUERY_DUPLICATE_CALENDARS = 1 shl 3 + private const val TOKEN_QUERY_REMINDERS = 1 shl 4 + private const val TOKEN_QUERY_VISIBLE_CALENDARS = 1 shl 5 + private const val TOKEN_QUERY_COLORS = 1 shl 6 + private const val TOKEN_QUERY_ALL = (TOKEN_QUERY_DUPLICATE_CALENDARS + or TOKEN_QUERY_ATTENDEES or TOKEN_QUERY_CALENDARS or TOKEN_QUERY_EVENT + or TOKEN_QUERY_REMINDERS or TOKEN_QUERY_VISIBLE_CALENDARS or TOKEN_QUERY_COLORS) + private val EVENT_PROJECTION = arrayOf<String>( + Events._ID, // 0 do not remove; used in DeleteEventHelper + Events.TITLE, // 1 do not remove; used in DeleteEventHelper + Events.RRULE, // 2 do not remove; used in DeleteEventHelper + Events.ALL_DAY, // 3 do not remove; used in DeleteEventHelper + Events.CALENDAR_ID, // 4 do not remove; used in DeleteEventHelper + Events.DTSTART, // 5 do not remove; used in DeleteEventHelper + Events._SYNC_ID, // 6 do not remove; used in DeleteEventHelper + Events.EVENT_TIMEZONE, // 7 do not remove; used in DeleteEventHelper + Events.DESCRIPTION, // 8 + Events.EVENT_LOCATION, // 9 + Calendars.CALENDAR_ACCESS_LEVEL, // 10 + Events.CALENDAR_COLOR, // 11 + Events.EVENT_COLOR, // 12 + Events.HAS_ATTENDEE_DATA, // 13 + Events.ORGANIZER, // 14 + Events.HAS_ALARM, // 15 + Calendars.MAX_REMINDERS, // 16 + Calendars.ALLOWED_REMINDERS, // 17 + Events.CUSTOM_APP_PACKAGE, // 18 + Events.CUSTOM_APP_URI, // 19 + Events.DTEND, // 20 + Events.DURATION, // 21 + Events.ORIGINAL_SYNC_ID // 22 do not remove; used in DeleteEventHelper + ) + private const val EVENT_INDEX_ID = 0 + private const val EVENT_INDEX_TITLE = 1 + private const val EVENT_INDEX_RRULE = 2 + private const val EVENT_INDEX_ALL_DAY = 3 + private const val EVENT_INDEX_CALENDAR_ID = 4 + private const val EVENT_INDEX_DTSTART = 5 + private const val EVENT_INDEX_SYNC_ID = 6 + private const val EVENT_INDEX_EVENT_TIMEZONE = 7 + private const val EVENT_INDEX_DESCRIPTION = 8 + private const val EVENT_INDEX_EVENT_LOCATION = 9 + private const val EVENT_INDEX_ACCESS_LEVEL = 10 + private const val EVENT_INDEX_CALENDAR_COLOR = 11 + private const val EVENT_INDEX_EVENT_COLOR = 12 + private const val EVENT_INDEX_HAS_ATTENDEE_DATA = 13 + private const val EVENT_INDEX_ORGANIZER = 14 + private const val EVENT_INDEX_HAS_ALARM = 15 + private const val EVENT_INDEX_MAX_REMINDERS = 16 + private const val EVENT_INDEX_ALLOWED_REMINDERS = 17 + private const val EVENT_INDEX_CUSTOM_APP_PACKAGE = 18 + private const val EVENT_INDEX_CUSTOM_APP_URI = 19 + private const val EVENT_INDEX_DTEND = 20 + private const val EVENT_INDEX_DURATION = 21 + val CALENDARS_PROJECTION = arrayOf<String>( + Calendars._ID, // 0 + Calendars.CALENDAR_DISPLAY_NAME, // 1 + Calendars.OWNER_ACCOUNT, // 2 + Calendars.CAN_ORGANIZER_RESPOND, // 3 + Calendars.ACCOUNT_NAME, // 4 + Calendars.ACCOUNT_TYPE // 5 + ) + const val CALENDARS_INDEX_DISPLAY_NAME = 1 + const val CALENDARS_INDEX_OWNER_ACCOUNT = 2 + const val CALENDARS_INDEX_OWNER_CAN_RESPOND = 3 + const val CALENDARS_INDEX_ACCOUNT_NAME = 4 + const val CALENDARS_INDEX_ACCOUNT_TYPE = 5 + val CALENDARS_WHERE: String = Calendars._ID.toString() + "=?" + val CALENDARS_DUPLICATE_NAME_WHERE: String = + Calendars.CALENDAR_DISPLAY_NAME.toString() + "=?" + val CALENDARS_VISIBLE_WHERE: String = Calendars.VISIBLE.toString() + "=?" + const val COLORS_INDEX_COLOR = 1 + const val COLORS_INDEX_COLOR_KEY = 2 + private var mScale = 0f // Used for supporting different screen densities + private var mCustomAppIconSize = 32 + private const val FADE_IN_TIME = 300 // in milliseconds + private const val LOADING_MSG_DELAY = 600 // in milliseconds + private const val LOADING_MSG_MIN_DISPLAY_TIME = 600 + private var mDialogWidth = 500 + private var mDialogHeight = 600 + private var DIALOG_TOP_MARGIN = 8 + fun getResponseFromButtonId(buttonId: Int): Int { + return Attendees.ATTENDEE_STATUS_NONE + } + + fun findButtonIdForResponse(response: Int): Int { + return -1 + } + + init { + if (!Utils.isJellybeanOrLater()) { + EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_PACKAGE] = Events._ID // nonessential value + EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_URI] = Events._ID // nonessential value + } + } + } + + private var mView: View? = null + private var mUri: Uri? = null + var eventId: Long = 0 + private set + private val mEventCursor: Cursor? = null + private val mCalendarsCursor: Cursor? = null + var startMillis: Long = 0 + private set + var endMillis: Long = 0 + private set + private var mAllDay = false + private var mOwnerCanRespond = false + private var mSyncAccountName: String? = null + private var mCalendarOwnerAccount: String? = null + private var mIsBusyFreeCalendar = false + private val mOriginalAttendeeResponse = 0 + private var mAttendeeResponseFromIntent: Int = Attendees.ATTENDEE_STATUS_NONE + private val mUserSetResponse: Int = Attendees.ATTENDEE_STATUS_NONE + private val mWhichEvents = -1 + + // Used as the temporary response until the dialog is confirmed. It is also + // able to be used as a state marker for configuration changes. + private val mTentativeUserSetResponse: Int = Attendees.ATTENDEE_STATUS_NONE + private var mHasAlarm = false + + // Used to prevent saving changes in event if it is being deleted. + private val mEventDeletionStarted = false + private var mTitle: TextView? = null + private var mWhenDateTime: TextView? = null + private var mWhere: TextView? = null + private var mMenu: Menu? = null + private var mHeadlines: View? = null + private var mScrollView: ScrollView? = null + private var mLoadingMsgView: View? = null + private var mErrorMsgView: View? = null + private var mAnimateAlpha: ObjectAnimator? = null + private var mLoadingMsgStartTime: Long = 0 + private val mDisplayColorKeyMap: SparseIntArray = SparseIntArray() + private val mOriginalColor = -1 + private val mOriginalColorInitialized = false + private val mCalendarColor = -1 + private val mCalendarColorInitialized = false + private val mCurrentColor = -1 + private val mCurrentColorInitialized = false + private val mCurrentColorKey = -1 + private var mNoCrossFade = false // Used to prevent repeated cross-fade + private var mResponseRadioGroup: RadioGroup? = null + var mToEmails: ArrayList<String> = ArrayList<String>() + var mCcEmails: ArrayList<String> = ArrayList<String>() + private val mTZUpdater: Runnable = object : Runnable { + @Override + override fun run() { + updateEvent(mView) + } + } + private val mLoadingMsgAlphaUpdater: Runnable = object : Runnable { + @Override + override fun run() { + // Since this is run after a delay, make sure to only show the message + // if the event's data is not shown yet. + if (!mAnimateAlpha!!.isRunning() && mScrollView!!.getAlpha() == 0f) { + mLoadingMsgStartTime = System.currentTimeMillis() + mLoadingMsgView?.setAlpha(1f) + } + } + } + private var mIsDialog = false + private var mIsPaused = true + private val mDismissOnResume = false + private var mX = -1 + private var mY = -1 + private var mMinTop = 0 // Dialog cannot be above this location + private var mIsTabletConfig = false + private var mActivity: Activity? = null + private var mContext: Context? = null + private var mController: CalendarController? = null + private fun sendAccessibilityEventIfQueryDone(token: Int) { + mCurrentQuery = mCurrentQuery or token + if (mCurrentQuery == TOKEN_QUERY_ALL) { + sendAccessibilityEvent() + } + } + + constructor( + context: Context, + uri: Uri?, + startMillis: Long, + endMillis: Long, + attendeeResponse: Int, + isDialog: Boolean, + windowStyle: Int + ) { + val r: Resources = context.getResources() + if (mScale == 0f) { + mScale = context.getResources().getDisplayMetrics().density + if (mScale != 1f) { + mCustomAppIconSize *= mScale.toInt() + if (isDialog) { + DIALOG_TOP_MARGIN *= mScale.toInt() + } + } + } + if (isDialog) { + setDialogSize(r) + } + mIsDialog = isDialog + setStyle(DialogFragment.STYLE_NO_TITLE, 0) + mUri = uri + this.startMillis = startMillis + this.endMillis = endMillis + mAttendeeResponseFromIntent = attendeeResponse + mWindowStyle = windowStyle + } + + // This is currently required by the fragment manager. + constructor() {} + constructor( + context: Context?, + eventId: Long, + startMillis: Long, + endMillis: Long, + attendeeResponse: Int, + isDialog: Boolean, + windowStyle: Int + ) : this( + context as Context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis, + endMillis, attendeeResponse, isDialog, windowStyle + ) { + this.eventId = eventId + } + + @Override + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + if (mIsDialog) { + applyDialogParams() + } + val activity: Activity = getActivity() + mContext = activity + } + + private fun applyDialogParams() { + val dialog: Dialog = getDialog() + dialog.setCanceledOnTouchOutside(true) + val window: Window? = dialog.getWindow() + window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + val a: WindowManager.LayoutParams? = window?.getAttributes() + a!!.dimAmount = .4f + a!!.width = mDialogWidth + a!!.height = mDialogHeight + + // On tablets , do smart positioning of dialog + // On phones , use the whole screen + if (mX != -1 || mY != -1) { + a!!.x = mX - mDialogWidth / 2 + a!!.y = mY - mDialogHeight / 2 + if (a!!.y < mMinTop) { + a!!.y = mMinTop + DIALOG_TOP_MARGIN + } + a!!.gravity = Gravity.LEFT or Gravity.TOP + } + window?.setAttributes(a) + } + + fun setDialogParams(x: Int, y: Int, minTop: Int) { + mX = x + mY = y + mMinTop = minTop + } + + // Implements OnCheckedChangeListener + @Override + override fun onCheckedChanged(group: RadioGroup?, checkedId: Int) { + } + + fun onNothingSelected(parent: AdapterView<*>?) {} + @Override + override fun onDetach() { + super.onDetach() + mController?.deregisterEventHandler(R.layout.event_info) + } + + @Override + override fun onAttach(activity: Activity?) { + super.onAttach(activity) + mActivity = activity + // Ensure that mIsTabletConfig is set before creating the menu. + mIsTabletConfig = Utils.getConfigBool(mActivity as Context, R.bool.tablet_config) + mController = CalendarController.getInstance(mActivity) + mController?.registerEventHandler(R.layout.event_info, this) + if (!mIsDialog) { + setHasOptionsMenu(true) + } + } + + @Override + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + mView = if (mWindowStyle == DIALOG_WINDOW_STYLE) { + inflater.inflate(R.layout.event_info_dialog, container, false) + } else { + inflater.inflate(R.layout.event_info, container, false) + } + mScrollView = mView?.findViewById(R.id.event_info_scroll_view) as ScrollView + mLoadingMsgView = mView?.findViewById(R.id.event_info_loading_msg) + mErrorMsgView = mView?.findViewById(R.id.event_info_error_msg) + mTitle = mView?.findViewById(R.id.title) as TextView + mWhenDateTime = mView?.findViewById(R.id.when_datetime) as TextView + mWhere = mView?.findViewById(R.id.where) as TextView + mHeadlines = mView?.findViewById(R.id.event_info_headline) + mResponseRadioGroup = mView?.findViewById(R.id.response_value) as RadioGroup + mAnimateAlpha = ObjectAnimator.ofFloat(mScrollView, "Alpha", 0f, 1f) + mAnimateAlpha?.setDuration(FADE_IN_TIME.toLong()) + mAnimateAlpha?.addListener(object : AnimatorListenerAdapter() { + var defLayerType = 0 + @Override + override fun onAnimationStart(animation: Animator) { + // Use hardware layer for better performance during animation + defLayerType = mScrollView?.getLayerType() as Int + mScrollView?.setLayerType(View.LAYER_TYPE_HARDWARE, null) + // Ensure that the loading message is gone before showing the + // event info + mLoadingMsgView?.removeCallbacks(mLoadingMsgAlphaUpdater) + mLoadingMsgView?.setVisibility(View.GONE) + } + + @Override + override fun onAnimationCancel(animation: Animator) { + mScrollView?.setLayerType(defLayerType, null) + } + + @Override + override fun onAnimationEnd(animation: Animator) { + mScrollView?.setLayerType(defLayerType, null) + // Do not cross fade after the first time + mNoCrossFade = true + } + }) + mLoadingMsgView?.setAlpha(0f) + mScrollView?.setAlpha(0f) + mErrorMsgView?.setVisibility(View.INVISIBLE) + mLoadingMsgView?.postDelayed(mLoadingMsgAlphaUpdater, LOADING_MSG_DELAY.toLong()) + + // Hide Edit/Delete buttons if in full screen mode on a phone + if (!mIsDialog && !mIsTabletConfig || mWindowStyle == FULL_WINDOW_STYLE) { + mView?.findViewById<View>(R.id.event_info_buttons_container)?.setVisibility(View.GONE) + } + return mView + } + + private fun updateTitle() { + val res: Resources = getActivity().getResources() + getActivity().setTitle(res.getString(R.string.event_info_title)) + } + + /** + * Initializes the event cursor, which is expected to point to the first + * (and only) result from a query. + * @return false if the cursor is empty, true otherwise + */ + private fun initEventCursor(): Boolean { + if (mEventCursor == null || mEventCursor.getCount() === 0) { + return false + } + mEventCursor.moveToFirst() + eventId = mEventCursor.getInt(EVENT_INDEX_ID).toLong() + val rRule: String = mEventCursor.getString(EVENT_INDEX_RRULE) + // mHasAlarm will be true if it was saved in the event already. + mHasAlarm = if (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) === 1) true else false + return true + } + + @Override + override fun onSaveInstanceState(outState: Bundle?) { + super.onSaveInstanceState(outState) + } + + @Override + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + // Show color/edit/delete buttons only in non-dialog configuration + if (!mIsDialog && !mIsTabletConfig || mWindowStyle == FULL_WINDOW_STYLE) { + inflater.inflate(R.menu.event_info_title_bar, menu) + mMenu = menu + } + } + + @Override + override fun onOptionsItemSelected(item: MenuItem): Boolean { + + // If we're a dialog we don't want to handle menu buttons + if (mIsDialog) { + return false + } + // Handles option menu selections: + // Home button - close event info activity and start the main calendar + // one + // Edit button - start the event edit activity and close the info + // activity + // Delete button - start a delete query that calls a runnable that close + // the info activity + val itemId: Int = item.getItemId() + if (itemId == android.R.id.home) { + Utils.returnToCalendarHome(mContext as Context) + mActivity?.finish() + return true + } else if (itemId == R.id.info_action_edit) { + mActivity?.finish() + } + return super.onOptionsItemSelected(item) + } + + @Override + override fun onStop() { + super.onStop() + } + + @Override + override fun onDestroy() { + if (mEventCursor != null) { + mEventCursor.close() + } + if (mCalendarsCursor != null) { + mCalendarsCursor.close() + } + super.onDestroy() + } + + /** + * Creates an exception to a recurring event. The only change we're making is to the + * "self attendee status" value. The provider will take care of updating the corresponding + * Attendees.attendeeStatus entry. + * + * @param eventId The recurring event. + * @param status The new value for selfAttendeeStatus. + */ + private fun createExceptionResponse(eventId: Long, status: Int) { + val values = ContentValues() + values.put(Events.ORIGINAL_INSTANCE_TIME, startMillis) + values.put(Events.SELF_ATTENDEE_STATUS, status) + values.put(Events.STATUS, Events.STATUS_CONFIRMED) + val ops: ArrayList<ContentProviderOperation> = ArrayList<ContentProviderOperation>() + val exceptionUri: Uri = Uri.withAppendedPath( + Events.CONTENT_EXCEPTION_URI, + eventId.toString() + ) + ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build()) + } + + private fun displayEventNotFound() { + mErrorMsgView?.setVisibility(View.VISIBLE) + mScrollView?.setVisibility(View.GONE) + mLoadingMsgView?.setVisibility(View.GONE) + } + + private fun updateEvent(view: View?) { + if (mEventCursor == null || view == null) { + return + } + val context: Context = view.getContext() ?: return + var eventName: String = mEventCursor.getString(EVENT_INDEX_TITLE) + if (eventName == null || eventName.length == 0) { + eventName = getActivity().getString(R.string.no_title_label) + } + + // 3rd parties might not have specified the start/end time when firing the + // Events.CONTENT_URI intent. Update these with values read from the db. + if (startMillis == 0L && endMillis == 0L) { + startMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART) + endMillis = mEventCursor.getLong(EVENT_INDEX_DTEND) + if (endMillis == 0L) { + val duration: String = mEventCursor.getString(EVENT_INDEX_DURATION) + if (!TextUtils.isEmpty(duration)) { + try { + val d = Duration() + d.parse(duration) + val endMillis: Long = startMillis + d.getMillis() + if (endMillis >= startMillis) { + this.endMillis = endMillis + } else { + Log.d(TAG, "Invalid duration string: $duration") + } + } catch (e: DateException) { + Log.d(TAG, "Error parsing duration string $duration", e) + } + } + if (endMillis == 0L) { + endMillis = startMillis + } + } + } + mAllDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) !== 0 + val location: String = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION) + val description: String = mEventCursor.getString(EVENT_INDEX_DESCRIPTION) + val rRule: String = mEventCursor.getString(EVENT_INDEX_RRULE) + val eventTimezone: String = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE) + mHeadlines?.setBackgroundColor(mCurrentColor) + + // What + if (eventName != null) { + setTextCommon(view, R.id.title, eventName) + } + + // When + // Set the date and repeats (if any) + val localTimezone: String? = Utils.getTimeZone(mActivity, mTZUpdater) + val resources: Resources = context.getResources() + var displayedDatetime: String? = Utils.getDisplayedDatetime( + startMillis, endMillis, + System.currentTimeMillis(), localTimezone as String, mAllDay, context + ) + var displayedTimezone: String? = null + if (!mAllDay) { + displayedTimezone = Utils.getDisplayedTimezone( + startMillis, localTimezone, + eventTimezone + ) + } + // Display the datetime. Make the timezone (if any) transparent. + if (displayedTimezone == null) { + setTextCommon(view, R.id.when_datetime, displayedDatetime as CharSequence) + } else { + val timezoneIndex: Int = displayedDatetime!!.length + displayedDatetime += " $displayedTimezone" + val sb = SpannableStringBuilder(displayedDatetime) + val transparentColorSpan = ForegroundColorSpan( + resources.getColor(R.color.event_info_headline_transparent_color) + ) + sb.setSpan( + transparentColorSpan, timezoneIndex, displayedDatetime!!.length, + Spannable.SPAN_INCLUSIVE_INCLUSIVE + ) + setTextCommon(view, R.id.when_datetime, sb) + } + view.findViewById<View>(R.id.when_repeat).setVisibility(View.GONE) + + // Organizer view is setup in the updateCalendar method + + // Where + if (location == null || location.trim().length == 0) { + setVisibilityCommon(view, R.id.where, View.GONE) + } else { + val textView: TextView? = mWhere + if (textView != null) { + textView.setText(location.trim()) + } + } + + // Launch Custom App + if (Utils.isJellybeanOrLater()) { + updateCustomAppButton() + } + } + + private fun updateCustomAppButton() { + setVisibilityCommon(mView, R.id.launch_custom_app_container, View.GONE) + return + } + + private fun sendAccessibilityEvent() { + val am: AccessibilityManager = + getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager + if (!am.isEnabled()) { + return + } + val event: AccessibilityEvent = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED) + event.setClassName(EventInfoFragment::class.java.getName()) + event.setPackageName(getActivity().getPackageName()) + var text = event.getText() + if (mResponseRadioGroup?.getVisibility() == View.VISIBLE) { + val id: Int = mResponseRadioGroup!!.getCheckedRadioButtonId() + if (id != View.NO_ID) { + text.add((getView()?.findViewById(R.id.response_label) as TextView)?.getText()) + text.add( + (mResponseRadioGroup?.findViewById(id) as RadioButton) + .getText().toString() + PERIOD_SPACE + ) + } + } + am.sendAccessibilityEvent(event) + } + + private fun updateCalendar(view: View?) { + mCalendarOwnerAccount = "" + if (mCalendarsCursor != null && mEventCursor != null) { + mCalendarsCursor.moveToFirst() + val tempAccount: String = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT) + mCalendarOwnerAccount = tempAccount ?: "" + mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) !== 0 + mSyncAccountName = mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME) + setVisibilityCommon(view, R.id.organizer_container, View.GONE) + mIsBusyFreeCalendar = + mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) === Calendars.CAL_ACCESS_FREEBUSY + if (!mIsBusyFreeCalendar) { + val b: View? = mView?.findViewById(R.id.edit) + b?.setEnabled(true) + b?.setOnClickListener(object : OnClickListener { + @Override + override fun onClick(v: View?) { + // For dialogs, just close the fragment + // For full screen, close activity on phone, leave it for tablet + if (mIsDialog) { + this@EventInfoFragment.dismiss() + } else if (!mIsTabletConfig) { + getActivity().finish() + } + } + }) + } + var button: View + if ((!mIsDialog && !mIsTabletConfig || + mWindowStyle == FULL_WINDOW_STYLE) && mMenu != null + ) { + mActivity?.invalidateOptionsMenu() + } + } else { + setVisibilityCommon(view, R.id.calendar, View.GONE) + sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS) + } + } + + private fun setTextCommon(view: View, id: Int, text: CharSequence) { + val textView: TextView = view.findViewById(id) as TextView ?: return + textView.setText(text) + } + + private fun setVisibilityCommon(view: View?, id: Int, visibility: Int) { + val v: View? = view?.findViewById(id) + if (v != null) { + v.setVisibility(visibility) + } + return + } + + @Override + override fun onPause() { + mIsPaused = true + super.onPause() + } + + @Override + override fun onResume() { + super.onResume() + if (mIsDialog) { + setDialogSize(getActivity().getResources()) + applyDialogParams() + } + mIsPaused = false + if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) { + val buttonId = findButtonIdForResponse(mTentativeUserSetResponse) + mResponseRadioGroup?.check(buttonId) + } + } + + @Override + override fun eventsChanged() { + } + + @get:Override override val supportedEventTypes: Long + get() = EventType.EVENTS_CHANGED + + @Override + override fun handleEvent(event: EventInfo?) { + reloadEvents() + } + + fun reloadEvents() {} + @Override + override fun onClick(view: View?) { + } + + private fun setDialogSize(r: Resources) { + mDialogWidth = r.getDimension(R.dimen.event_info_dialog_width).toInt() + mDialogHeight = r.getDimension(R.dimen.event_info_dialog_height).toInt() + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/EventLoader.java b/src/com/android/calendar/EventLoader.java deleted file mode 100644 index d34b1c7c..00000000 --- a/src/com/android/calendar/EventLoader.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.calendar; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.os.Handler; -import android.os.Process; -import android.provider.CalendarContract; -import android.provider.CalendarContract.EventDays; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicInteger; - -public class EventLoader { - - private Context mContext; - private Handler mHandler = new Handler(); - private AtomicInteger mSequenceNumber = new AtomicInteger(); - - private LinkedBlockingQueue<LoadRequest> mLoaderQueue; - private LoaderThread mLoaderThread; - private ContentResolver mResolver; - - private static interface LoadRequest { - public void processRequest(EventLoader eventLoader); - public void skipRequest(EventLoader eventLoader); - } - - private static class ShutdownRequest implements LoadRequest { - public void processRequest(EventLoader eventLoader) { - } - - public void skipRequest(EventLoader eventLoader) { - } - } - - /** - * - * Code for handling requests to get whether days have an event or not - * and filling in the eventDays array. - * - */ - private static class LoadEventDaysRequest implements LoadRequest { - public int startDay; - public int numDays; - public boolean[] eventDays; - public Runnable uiCallback; - - /** - * The projection used by the EventDays query. - */ - private static final String[] PROJECTION = { - CalendarContract.EventDays.STARTDAY, CalendarContract.EventDays.ENDDAY - }; - - public LoadEventDaysRequest(int startDay, int numDays, boolean[] eventDays, - final Runnable uiCallback) - { - this.startDay = startDay; - this.numDays = numDays; - this.eventDays = eventDays; - this.uiCallback = uiCallback; - } - - @Override - public void processRequest(EventLoader eventLoader) - { - final Handler handler = eventLoader.mHandler; - ContentResolver cr = eventLoader.mResolver; - - // Clear the event days - Arrays.fill(eventDays, false); - - //query which days have events - Cursor cursor = EventDays.query(cr, startDay, numDays, PROJECTION); - try { - int startDayColumnIndex = cursor.getColumnIndexOrThrow(EventDays.STARTDAY); - int endDayColumnIndex = cursor.getColumnIndexOrThrow(EventDays.ENDDAY); - - //Set all the days with events to true - while (cursor.moveToNext()) { - int firstDay = cursor.getInt(startDayColumnIndex); - int lastDay = cursor.getInt(endDayColumnIndex); - //we want the entire range the event occurs, but only within the month - int firstIndex = Math.max(firstDay - startDay, 0); - int lastIndex = Math.min(lastDay - startDay, 30); - - for(int i = firstIndex; i <= lastIndex; i++) { - eventDays[i] = true; - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - handler.post(uiCallback); - } - - @Override - public void skipRequest(EventLoader eventLoader) { - } - } - - private static class LoadEventsRequest implements LoadRequest { - - public int id; - public int startDay; - public int numDays; - public ArrayList<Event> events; - public Runnable successCallback; - public Runnable cancelCallback; - - public LoadEventsRequest(int id, int startDay, int numDays, ArrayList<Event> events, - final Runnable successCallback, final Runnable cancelCallback) { - this.id = id; - this.startDay = startDay; - this.numDays = numDays; - this.events = events; - this.successCallback = successCallback; - this.cancelCallback = cancelCallback; - } - - public void processRequest(EventLoader eventLoader) { - Event.loadEvents(eventLoader.mContext, events, startDay, - numDays, id, eventLoader.mSequenceNumber); - - // Check if we are still the most recent request. - if (id == eventLoader.mSequenceNumber.get()) { - eventLoader.mHandler.post(successCallback); - } else { - eventLoader.mHandler.post(cancelCallback); - } - } - - public void skipRequest(EventLoader eventLoader) { - eventLoader.mHandler.post(cancelCallback); - } - } - - private static class LoaderThread extends Thread { - LinkedBlockingQueue<LoadRequest> mQueue; - EventLoader mEventLoader; - - public LoaderThread(LinkedBlockingQueue<LoadRequest> queue, EventLoader eventLoader) { - mQueue = queue; - mEventLoader = eventLoader; - } - - public void shutdown() { - try { - mQueue.put(new ShutdownRequest()); - } catch (InterruptedException ex) { - // The put() method fails with InterruptedException if the - // queue is full. This should never happen because the queue - // has no limit. - Log.e("Cal", "LoaderThread.shutdown() interrupted!"); - } - } - - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - while (true) { - try { - // Wait for the next request - LoadRequest request = mQueue.take(); - - // If there are a bunch of requests already waiting, then - // skip all but the most recent request. - while (!mQueue.isEmpty()) { - // Let the request know that it was skipped - request.skipRequest(mEventLoader); - - // Skip to the next request - request = mQueue.take(); - } - - if (request instanceof ShutdownRequest) { - return; - } - request.processRequest(mEventLoader); - } catch (InterruptedException ex) { - Log.e("Cal", "background LoaderThread interrupted!"); - } - } - } - } - - public EventLoader(Context context) { - mContext = context; - mLoaderQueue = new LinkedBlockingQueue<LoadRequest>(); - mResolver = context.getContentResolver(); - } - - /** - * Call this from the activity's onResume() - */ - public void startBackgroundThread() { - mLoaderThread = new LoaderThread(mLoaderQueue, this); - mLoaderThread.start(); - } - - /** - * Call this from the activity's onPause() - */ - public void stopBackgroundThread() { - mLoaderThread.shutdown(); - } - - /** - * Loads "numDays" days worth of events, starting at start, into events. - * Posts uiCallback to the {@link Handler} for this view, which will run in the UI thread. - * Reuses an existing background thread, if events were already being loaded in the background. - * NOTE: events and uiCallback are not used if an existing background thread gets reused -- - * the ones that were passed in on the call that results in the background thread getting - * created are used, and the most recent call's worth of data is loaded into events and posted - * via the uiCallback. - */ - public void loadEventsInBackground(final int numDays, final ArrayList<Event> events, - int startDay, final Runnable successCallback, final Runnable cancelCallback) { - - // Increment the sequence number for requests. We don't care if the - // sequence numbers wrap around because we test for equality with the - // latest one. - int id = mSequenceNumber.incrementAndGet(); - - // Send the load request to the background thread - LoadEventsRequest request = new LoadEventsRequest(id, startDay, numDays, - events, successCallback, cancelCallback); - - try { - mLoaderQueue.put(request); - } catch (InterruptedException ex) { - // The put() method fails with InterruptedException if the - // queue is full. This should never happen because the queue - // has no limit. - Log.e("Cal", "loadEventsInBackground() interrupted!"); - } - } - - /** - * Sends a request for the days with events to be marked. Loads "numDays" - * worth of days, starting at start, and fills in eventDays to express which - * days have events. - * - * @param startDay First day to check for events - * @param numDays Days following the start day to check - * @param eventDay Whether or not an event exists on that day - * @param uiCallback What to do when done (log data, redraw screen) - */ - void loadEventDaysInBackground(int startDay, int numDays, boolean[] eventDays, - final Runnable uiCallback) - { - // Send load request to the background thread - LoadEventDaysRequest request = new LoadEventDaysRequest(startDay, numDays, - eventDays, uiCallback); - try { - mLoaderQueue.put(request); - } catch (InterruptedException ex) { - // The put() method fails with InterruptedException if the - // queue is full. This should never happen because the queue - // has no limit. - Log.e("Cal", "loadEventDaysInBackground() interrupted!"); - } - } -} diff --git a/src/com/android/calendar/EventLoader.kt b/src/com/android/calendar/EventLoader.kt new file mode 100644 index 00000000..a05e8a2e --- /dev/null +++ b/src/com/android/calendar/EventLoader.kt @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.os.Handler +import android.os.Process +import android.provider.CalendarContract +import android.provider.CalendarContract.EventDays +import android.util.Log +import java.util.ArrayList +import java.util.Arrays +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.atomic.AtomicInteger + +class EventLoader(context: Context) { + private val mContext: Context + private val mHandler: Handler = Handler() + private val mSequenceNumber: AtomicInteger? = AtomicInteger() + private val mLoaderQueue: LinkedBlockingQueue<LoadRequest> + private var mLoaderThread: LoaderThread? = null + private val mResolver: ContentResolver + + private interface LoadRequest { + fun processRequest(eventLoader: EventLoader?) + fun skipRequest(eventLoader: EventLoader?) + } + + private class ShutdownRequest : LoadRequest { + override fun processRequest(eventLoader: EventLoader?) {} + override fun skipRequest(eventLoader: EventLoader?) {} + } + + /** + * + * Code for handling requests to get whether days have an event or not + * and filling in the eventDays array. + * + */ + private class LoadEventDaysRequest( + var startDay: Int, + var numDays: Int, + var eventDays: BooleanArray, + uiCallback: Runnable + ) : LoadRequest { + var uiCallback: Runnable + @Override + override fun processRequest(eventLoader: EventLoader?) { + val handler: Handler? = eventLoader?.mHandler + val cr: ContentResolver? = eventLoader?.mResolver + + // Clear the event days + Arrays.fill(eventDays, false) + + // query which days have events + val cursor: Cursor = EventDays.query(cr, startDay, numDays, PROJECTION) + try { + val startDayColumnIndex: Int = cursor.getColumnIndexOrThrow(EventDays.STARTDAY) + val endDayColumnIndex: Int = cursor.getColumnIndexOrThrow(EventDays.ENDDAY) + + // Set all the days with events to true + while (cursor.moveToNext()) { + val firstDay: Int = cursor.getInt(startDayColumnIndex) + val lastDay: Int = cursor.getInt(endDayColumnIndex) + // we want the entire range the event occurs, but only within the month + val firstIndex: Int = Math.max(firstDay - startDay, 0) + val lastIndex: Int = Math.min(lastDay - startDay, 30) + for (i in firstIndex..lastIndex) { + eventDays[i] = true + } + } + } finally { + if (cursor != null) { + cursor.close() + } + } + handler?.post(uiCallback) + } + + @Override + override fun skipRequest(eventLoader: EventLoader?) { + } + + companion object { + /** + * The projection used by the EventDays query. + */ + private val PROJECTION = arrayOf<String>( + CalendarContract.EventDays.STARTDAY, CalendarContract.EventDays.ENDDAY + ) + } + + init { + this.uiCallback = uiCallback + } + } + + private class LoadEventsRequest( + var id: Int, + var startDay: Int, + var numDays: Int, + events: ArrayList<Event?>, + successCallback: Runnable, + cancelCallback: Runnable + ) : LoadRequest { + var events: ArrayList<Event?> + var successCallback: Runnable + var cancelCallback: Runnable + @Override + override fun processRequest(eventLoader: EventLoader?) { + Event.loadEvents(eventLoader?.mContext, events, startDay, + numDays, id, eventLoader?.mSequenceNumber) + + // Check if we are still the most recent request. + if (id == eventLoader?.mSequenceNumber?.get()) { + eventLoader?.mHandler?.post(successCallback) + } else { + eventLoader?.mHandler?.post(cancelCallback) + } + } + + @Override + override fun skipRequest(eventLoader: EventLoader?) { + eventLoader?.mHandler?.post(cancelCallback) + } + + init { + this.events = events + this.successCallback = successCallback + this.cancelCallback = cancelCallback + } + } + + private class LoaderThread( + queue: LinkedBlockingQueue<LoadRequest>, + eventLoader: EventLoader + ) : Thread() { + var mQueue: LinkedBlockingQueue<LoadRequest> + var mEventLoader: EventLoader + fun shutdown() { + try { + mQueue.put(ShutdownRequest()) + } catch (ex: InterruptedException) { + // The put() method fails with InterruptedException if the + // queue is full. This should never happen because the queue + // has no limit. + Log.e("Cal", "LoaderThread.shutdown() interrupted!") + } + } + + @Override + override fun run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) + while (true) { + try { + // Wait for the next request + var request: LoadRequest = mQueue.take() + + // If there are a bunch of requests already waiting, then + // skip all but the most recent request. + while (!mQueue.isEmpty()) { + // Let the request know that it was skipped + request.skipRequest(mEventLoader) + + // Skip to the next request + request = mQueue.take() + } + if (request is ShutdownRequest) { + return + } + request.processRequest(mEventLoader) + } catch (ex: InterruptedException) { + Log.e("Cal", "background LoaderThread interrupted!") + } + } + } + + init { + mQueue = queue + mEventLoader = eventLoader + } + } + + /** + * Call this from the activity's onResume() + */ + fun startBackgroundThread() { + mLoaderThread = LoaderThread(mLoaderQueue, this) + mLoaderThread?.start() + } + + /** + * Call this from the activity's onPause() + */ + fun stopBackgroundThread() { + mLoaderThread!!.shutdown() + } + + /** + * Loads "numDays" days worth of events, starting at start, into events. + * Posts uiCallback to the [Handler] for this view, which will run in the UI thread. + * Reuses an existing background thread, if events were already being loaded in the background. + * NOTE: events and uiCallback are not used if an existing background thread gets reused -- + * the ones that were passed in on the call that results in the background thread getting + * created are used, and the most recent call's worth of data is loaded into events and posted + * via the uiCallback. + */ + fun loadEventsInBackground( + numDays: Int, + events: ArrayList<Event?>, + startDay: Int, + successCallback: Runnable, + cancelCallback: Runnable + ) { + + // Increment the sequence number for requests. We don't care if the + // sequence numbers wrap around because we test for equality with the + // latest one. + val id: Int = mSequenceNumber?.incrementAndGet() as Int + + // Send the load request to the background thread + val request = LoadEventsRequest(id, startDay, numDays, + events, successCallback, cancelCallback) + try { + mLoaderQueue.put(request) + } catch (ex: InterruptedException) { + // The put() method fails with InterruptedException if the + // queue is full. This should never happen because the queue + // has no limit. + Log.e("Cal", "loadEventsInBackground() interrupted!") + } + } + + /** + * Sends a request for the days with events to be marked. Loads "numDays" + * worth of days, starting at start, and fills in eventDays to express which + * days have events. + * + * @param startDay First day to check for events + * @param numDays Days following the start day to check + * @param eventDay Whether or not an event exists on that day + * @param uiCallback What to do when done (log data, redraw screen) + */ + fun loadEventDaysInBackground( + startDay: Int, + numDays: Int, + eventDays: BooleanArray, + uiCallback: Runnable + ) { + // Send load request to the background thread + val request = LoadEventDaysRequest(startDay, numDays, + eventDays, uiCallback) + try { + mLoaderQueue.put(request) + } catch (ex: InterruptedException) { + // The put() method fails with InterruptedException if the + // queue is full. This should never happen because the queue + // has no limit. + Log.e("Cal", "loadEventDaysInBackground() interrupted!") + } + } + + init { + mContext = context + mLoaderQueue = LinkedBlockingQueue<LoadRequest>() + mResolver = context.getContentResolver() + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/GeneralPreferences.java b/src/com/android/calendar/GeneralPreferences.java deleted file mode 100644 index a42f07e3..00000000 --- a/src/com/android/calendar/GeneralPreferences.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Copyright (C) 2007 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.calendar; - -import android.app.Activity; -import android.app.FragmentManager; -import android.app.backup.BackupManager; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.Vibrator; -import android.preference.CheckBoxPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.Preference.OnPreferenceClickListener; -import android.preference.PreferenceCategory; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; -import android.preference.PreferenceScreen; -import android.preference.RingtonePreference; -import android.provider.CalendarContract; -import android.provider.CalendarContract.CalendarCache; -import android.provider.SearchRecentSuggestions; -import android.text.TextUtils; -import android.text.format.Time; -import android.widget.Toast; - -import com.android.calendar.alerts.AlertReceiver; -import com.android.timezonepicker.TimeZoneInfo; -import com.android.timezonepicker.TimeZonePickerDialog; -import com.android.timezonepicker.TimeZonePickerDialog.OnTimeZoneSetListener; -import com.android.timezonepicker.TimeZonePickerUtils; - -public class GeneralPreferences extends PreferenceFragment implements - OnSharedPreferenceChangeListener, OnPreferenceChangeListener, OnTimeZoneSetListener { - // The name of the shared preferences file. This name must be maintained for historical - // reasons, as it's what PreferenceManager assigned the first time the file was created. - static final String SHARED_PREFS_NAME = "com.android.calendar_preferences"; - static final String SHARED_PREFS_NAME_NO_BACKUP = "com.android.calendar_preferences_no_backup"; - - private static final String FRAG_TAG_TIME_ZONE_PICKER = "TimeZonePicker"; - - // Preference keys - public static final String KEY_HIDE_DECLINED = "preferences_hide_declined"; - public static final String KEY_WEEK_START_DAY = "preferences_week_start_day"; - public static final String KEY_SHOW_WEEK_NUM = "preferences_show_week_num"; - public static final String KEY_DAYS_PER_WEEK = "preferences_days_per_week"; - public static final String KEY_SKIP_SETUP = "preferences_skip_setup"; - - public static final String KEY_CLEAR_SEARCH_HISTORY = "preferences_clear_search_history"; - - public static final String KEY_ALERTS_CATEGORY = "preferences_alerts_category"; - public static final String KEY_ALERTS = "preferences_alerts"; - public static final String KEY_ALERTS_VIBRATE = "preferences_alerts_vibrate"; - public static final String KEY_ALERTS_RINGTONE = "preferences_alerts_ringtone"; - public static final String KEY_ALERTS_POPUP = "preferences_alerts_popup"; - - public static final String KEY_SHOW_CONTROLS = "preferences_show_controls"; - - public static final String KEY_DEFAULT_REMINDER = "preferences_default_reminder"; - public static final int NO_REMINDER = -1; - public static final String NO_REMINDER_STRING = "-1"; - public static final int REMINDER_DEFAULT_TIME = 10; // in minutes - - public static final String KEY_DEFAULT_CELL_HEIGHT = "preferences_default_cell_height"; - public static final String KEY_VERSION = "preferences_version"; - - /** Key to SharePreference for default view (CalendarController.ViewType) */ - public static final String KEY_START_VIEW = "preferred_startView"; - /** - * Key to SharePreference for default detail view (CalendarController.ViewType) - * Typically used by widget - */ - public static final String KEY_DETAILED_VIEW = "preferred_detailedView"; - public static final String KEY_DEFAULT_CALENDAR = "preference_defaultCalendar"; - - // These must be in sync with the array preferences_week_start_day_values - public static final String WEEK_START_DEFAULT = "-1"; - public static final String WEEK_START_SATURDAY = "7"; - public static final String WEEK_START_SUNDAY = "1"; - public static final String WEEK_START_MONDAY = "2"; - - // These keys are kept to enable migrating users from previous versions - private static final String KEY_ALERTS_TYPE = "preferences_alerts_type"; - private static final String ALERT_TYPE_ALERTS = "0"; - private static final String ALERT_TYPE_STATUS_BAR = "1"; - private static final String ALERT_TYPE_OFF = "2"; - static final String KEY_HOME_TZ_ENABLED = "preferences_home_tz_enabled"; - static final String KEY_HOME_TZ = "preferences_home_tz"; - - // Default preference values - public static final int DEFAULT_START_VIEW = CalendarController.ViewType.WEEK; - public static final int DEFAULT_DETAILED_VIEW = CalendarController.ViewType.DAY; - public static final boolean DEFAULT_SHOW_WEEK_NUM = false; - // This should match the XML file. - public static final String DEFAULT_RINGTONE = "content://settings/system/notification_sound"; - - CheckBoxPreference mAlert; - CheckBoxPreference mVibrate; - CheckBoxPreference mPopup; - CheckBoxPreference mUseHomeTZ; - CheckBoxPreference mHideDeclined; - Preference mHomeTZ; - TimeZonePickerUtils mTzPickerUtils; - ListPreference mWeekStart; - ListPreference mDefaultReminder; - - private String mTimeZoneId; - - /** Return a properly configured SharedPreferences instance */ - public static SharedPreferences getSharedPreferences(Context context) { - return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); - } - - /** Set the default shared preferences in the proper context */ - public static void setDefaultValues(Context context) { - PreferenceManager.setDefaultValues(context, SHARED_PREFS_NAME, Context.MODE_PRIVATE, - R.xml.general_preferences, false); - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - final Activity activity = getActivity(); - - // Make sure to always use the same preferences file regardless of the package name - // we're running under - final PreferenceManager preferenceManager = getPreferenceManager(); - final SharedPreferences sharedPreferences = getSharedPreferences(activity); - preferenceManager.setSharedPreferencesName(SHARED_PREFS_NAME); - - // Load the preferences from an XML resource - addPreferencesFromResource(R.xml.general_preferences); - - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - mAlert = (CheckBoxPreference) preferenceScreen.findPreference(KEY_ALERTS); - mVibrate = (CheckBoxPreference) preferenceScreen.findPreference(KEY_ALERTS_VIBRATE); - Vibrator vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE); - if (vibrator == null || !vibrator.hasVibrator()) { - PreferenceCategory mAlertGroup = (PreferenceCategory) preferenceScreen - .findPreference(KEY_ALERTS_CATEGORY); - mAlertGroup.removePreference(mVibrate); - } - - mPopup = (CheckBoxPreference) preferenceScreen.findPreference(KEY_ALERTS_POPUP); - mUseHomeTZ = (CheckBoxPreference) preferenceScreen.findPreference(KEY_HOME_TZ_ENABLED); - mHideDeclined = (CheckBoxPreference) preferenceScreen.findPreference(KEY_HIDE_DECLINED); - mWeekStart = (ListPreference) preferenceScreen.findPreference(KEY_WEEK_START_DAY); - mDefaultReminder = (ListPreference) preferenceScreen.findPreference(KEY_DEFAULT_REMINDER); - mHomeTZ = preferenceScreen.findPreference(KEY_HOME_TZ); - mWeekStart.setSummary(mWeekStart.getEntry()); - mDefaultReminder.setSummary(mDefaultReminder.getEntry()); - - // This triggers an asynchronous call to the provider to refresh the data in shared pref - mTimeZoneId = Utils.getTimeZone(activity, null); - - SharedPreferences prefs = CalendarUtils.getSharedPreferences(activity, - Utils.SHARED_PREFS_NAME); - - // Utils.getTimeZone will return the currentTimeZone instead of the one - // in the shared_pref if home time zone is disabled. So if home tz is - // off, we will explicitly read it. - if (!prefs.getBoolean(KEY_HOME_TZ_ENABLED, false)) { - mTimeZoneId = prefs.getString(KEY_HOME_TZ, Time.getCurrentTimezone()); - } - - mHomeTZ.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - showTimezoneDialog(); - return true; - } - }); - - if (mTzPickerUtils == null) { - mTzPickerUtils = new TimeZonePickerUtils(getActivity()); - } - CharSequence timezoneName = mTzPickerUtils.getGmtDisplayName(getActivity(), mTimeZoneId, - System.currentTimeMillis(), false); - mHomeTZ.setSummary(timezoneName != null ? timezoneName : mTimeZoneId); - - TimeZonePickerDialog tzpd = (TimeZonePickerDialog) activity.getFragmentManager() - .findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER); - if (tzpd != null) { - tzpd.setOnTimeZoneSetListener(this); - } - - migrateOldPreferences(sharedPreferences); - - updateChildPreferences(); - } - - private void showTimezoneDialog() { - final Activity activity = getActivity(); - if (activity == null) { - return; - } - - Bundle b = new Bundle(); - b.putLong(TimeZonePickerDialog.BUNDLE_START_TIME_MILLIS, System.currentTimeMillis()); - b.putString(TimeZonePickerDialog.BUNDLE_TIME_ZONE, Utils.getTimeZone(activity, null)); - - FragmentManager fm = getActivity().getFragmentManager(); - TimeZonePickerDialog tzpd = (TimeZonePickerDialog) fm - .findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER); - if (tzpd != null) { - tzpd.dismiss(); - } - tzpd = new TimeZonePickerDialog(); - tzpd.setArguments(b); - tzpd.setOnTimeZoneSetListener(this); - tzpd.show(fm, FRAG_TAG_TIME_ZONE_PICKER); - } - - @Override - public void onStart() { - super.onStart(); - getPreferenceScreen().getSharedPreferences() - .registerOnSharedPreferenceChangeListener(this); - setPreferenceListeners(this); - } - - /** - * Sets up all the preference change listeners to use the specified - * listener. - */ - private void setPreferenceListeners(OnPreferenceChangeListener listener) { - mUseHomeTZ.setOnPreferenceChangeListener(listener); - mHomeTZ.setOnPreferenceChangeListener(listener); - mWeekStart.setOnPreferenceChangeListener(listener); - mDefaultReminder.setOnPreferenceChangeListener(listener); - mHideDeclined.setOnPreferenceChangeListener(listener); - mVibrate.setOnPreferenceChangeListener(listener); - } - - @Override - public void onStop() { - getPreferenceScreen().getSharedPreferences() - .unregisterOnSharedPreferenceChangeListener(this); - setPreferenceListeners(null); - super.onStop(); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - Activity a = getActivity(); - if (key.equals(KEY_ALERTS)) { - updateChildPreferences(); - if (a != null) { - Intent intent = new Intent(); - intent.setClass(a, AlertReceiver.class); - if (mAlert.isChecked()) { - intent.setAction(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS); - } else { - intent.setAction(AlertReceiver.EVENT_REMINDER_APP_ACTION); - } - a.sendBroadcast(intent); - } - } - if (a != null) { - BackupManager.dataChanged(a.getPackageName()); - } - } - - /** - * Handles time zone preference changes - */ - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String tz; - final Activity activity = getActivity(); - if (preference == mUseHomeTZ) { - if ((Boolean)newValue) { - tz = mTimeZoneId; - } else { - tz = CalendarCache.TIMEZONE_TYPE_AUTO; - } - Utils.setTimeZone(activity, tz); - return true; - } else if (preference == mHideDeclined) { - mHideDeclined.setChecked((Boolean) newValue); - Intent intent = new Intent(Utils.getWidgetScheduledUpdateAction(activity)); - intent.setDataAndType(CalendarContract.CONTENT_URI, Utils.APPWIDGET_DATA_TYPE); - activity.sendBroadcast(intent); - return true; - } else if (preference == mWeekStart) { - mWeekStart.setValue((String) newValue); - mWeekStart.setSummary(mWeekStart.getEntry()); - } else if (preference == mDefaultReminder) { - mDefaultReminder.setValue((String) newValue); - mDefaultReminder.setSummary(mDefaultReminder.getEntry()); - } else if (preference == mVibrate) { - mVibrate.setChecked((Boolean) newValue); - return true; - } else { - return true; - } - return false; - } - - public String getRingtoneTitleFromUri(Context context, String uri) { - if (TextUtils.isEmpty(uri)) { - return null; - } - - Ringtone ring = RingtoneManager.getRingtone(getActivity(), Uri.parse(uri)); - if (ring != null) { - return ring.getTitle(context); - } - return null; - } - - /** - * If necessary, upgrades previous versions of preferences to the current - * set of keys and values. - * @param prefs the preferences to upgrade - */ - private void migrateOldPreferences(SharedPreferences prefs) { - // If needed, migrate vibration setting from a previous version - - mVibrate.setChecked(Utils.getDefaultVibrate(getActivity(), prefs)); - - // If needed, migrate the old alerts type settin - if (!prefs.contains(KEY_ALERTS) && prefs.contains(KEY_ALERTS_TYPE)) { - String type = prefs.getString(KEY_ALERTS_TYPE, ALERT_TYPE_STATUS_BAR); - if (type.equals(ALERT_TYPE_OFF)) { - mAlert.setChecked(false); - mPopup.setChecked(false); - mPopup.setEnabled(false); - } else if (type.equals(ALERT_TYPE_STATUS_BAR)) { - mAlert.setChecked(true); - mPopup.setChecked(false); - mPopup.setEnabled(true); - } else if (type.equals(ALERT_TYPE_ALERTS)) { - mAlert.setChecked(true); - mPopup.setChecked(true); - mPopup.setEnabled(true); - } - // clear out the old setting - prefs.edit().remove(KEY_ALERTS_TYPE).commit(); - } - } - - /** - * Keeps the dependent settings in sync with the parent preference, so for - * example, when notifications are turned off, we disable the preferences - * for configuring the exact notification behavior. - */ - private void updateChildPreferences() { - if (mAlert.isChecked()) { - mVibrate.setEnabled(true); - mPopup.setEnabled(true); - } else { - mVibrate.setEnabled(false); - mPopup.setEnabled(false); - } - } - - - @Override - public boolean onPreferenceTreeClick( - PreferenceScreen preferenceScreen, Preference preference) { - final String key = preference.getKey(); - return super.onPreferenceTreeClick(preferenceScreen, preference); - } - - @Override - public void onTimeZoneSet(TimeZoneInfo tzi) { - if (mTzPickerUtils == null) { - mTzPickerUtils = new TimeZonePickerUtils(getActivity()); - } - - final CharSequence timezoneName = mTzPickerUtils.getGmtDisplayName( - getActivity(), tzi.mTzId, System.currentTimeMillis(), false); - mHomeTZ.setSummary(timezoneName); - Utils.setTimeZone(getActivity(), tzi.mTzId); - } -} diff --git a/src/com/android/calendar/GeneralPreferences.kt b/src/com/android/calendar/GeneralPreferences.kt new file mode 100644 index 00000000..dd4c9550 --- /dev/null +++ b/src/com/android/calendar/GeneralPreferences.kt @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.app.Activity +import android.app.FragmentManager +import android.app.backup.BackupManager +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.media.Ringtone +import android.media.RingtoneManager +import android.net.Uri +import android.os.Bundle +import android.os.Vibrator +import android.preference.CheckBoxPreference +import android.preference.ListPreference +import android.preference.Preference +import android.preference.Preference.OnPreferenceChangeListener +import android.preference.Preference.OnPreferenceClickListener +import android.preference.PreferenceCategory +import android.preference.PreferenceFragment +import android.preference.PreferenceManager +import android.preference.PreferenceScreen +import android.provider.CalendarContract +import android.provider.CalendarContract.CalendarCache +import android.text.TextUtils +import android.text.format.Time +import com.android.calendar.alerts.AlertReceiver +import com.android.timezonepicker.TimeZoneInfo +import com.android.timezonepicker.TimeZonePickerDialog +import com.android.timezonepicker.TimeZonePickerDialog.OnTimeZoneSetListener +import com.android.timezonepicker.TimeZonePickerUtils + +class GeneralPreferences : PreferenceFragment(), OnSharedPreferenceChangeListener, + OnPreferenceChangeListener, OnTimeZoneSetListener { + var mAlert: CheckBoxPreference? = null + var mVibrate: CheckBoxPreference? = null + var mPopup: CheckBoxPreference? = null + var mUseHomeTZ: CheckBoxPreference? = null + var mHideDeclined: CheckBoxPreference? = null + var mHomeTZ: Preference? = null + var mTzPickerUtils: TimeZonePickerUtils? = null + var mWeekStart: ListPreference? = null + var mDefaultReminder: ListPreference? = null + private var mTimeZoneId: String? = null + + @Override + override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + val activity: Activity = getActivity() + + // Make sure to always use the same preferences file regardless of the package name + // we're running under + val preferenceManager: PreferenceManager = getPreferenceManager() + val sharedPreferences: SharedPreferences? = getSharedPreferences(activity) + preferenceManager.setSharedPreferencesName(SHARED_PREFS_NAME) + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.general_preferences) + val preferenceScreen: PreferenceScreen = getPreferenceScreen() + mAlert = preferenceScreen.findPreference(KEY_ALERTS) as CheckBoxPreference + mVibrate = preferenceScreen.findPreference(KEY_ALERTS_VIBRATE) as CheckBoxPreference + val vibrator: Vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + if (vibrator == null || !vibrator.hasVibrator()) { + val mAlertGroup: PreferenceCategory = preferenceScreen + .findPreference(KEY_ALERTS_CATEGORY) as PreferenceCategory + mAlertGroup.removePreference(mVibrate) + } + mPopup = preferenceScreen.findPreference(KEY_ALERTS_POPUP) as CheckBoxPreference + mUseHomeTZ = preferenceScreen.findPreference(KEY_HOME_TZ_ENABLED) as CheckBoxPreference + mHideDeclined = preferenceScreen.findPreference(KEY_HIDE_DECLINED) as CheckBoxPreference + mWeekStart = preferenceScreen.findPreference(KEY_WEEK_START_DAY) as ListPreference + mDefaultReminder = preferenceScreen.findPreference(KEY_DEFAULT_REMINDER) as ListPreference + mHomeTZ = preferenceScreen.findPreference(KEY_HOME_TZ) + mWeekStart?.setSummary(mWeekStart?.getEntry()) + mDefaultReminder?.setSummary(mDefaultReminder?.getEntry()) + + // This triggers an asynchronous call to the provider to refresh the data in shared pref + mTimeZoneId = Utils.getTimeZone(activity, null) + val prefs: SharedPreferences = CalendarUtils.getSharedPreferences(activity, + Utils.SHARED_PREFS_NAME) + + // Utils.getTimeZone will return the currentTimeZone instead of the one + // in the shared_pref if home time zone is disabled. So if home tz is + // off, we will explicitly read it. + if (!prefs.getBoolean(KEY_HOME_TZ_ENABLED, false)) { + mTimeZoneId = prefs.getString(KEY_HOME_TZ, Time.getCurrentTimezone()) + } + + mHomeTZ?.setOnPreferenceClickListener(object : Preference.OnPreferenceClickListener { + @Override + override fun onPreferenceClick(preference: Preference?): Boolean { + showTimezoneDialog() + return true + } + }) + + if (mTzPickerUtils == null) { + mTzPickerUtils = TimeZonePickerUtils(getActivity()) + } + val timezoneName: CharSequence? = mTzPickerUtils?.getGmtDisplayName(getActivity(), + mTimeZoneId, System.currentTimeMillis(), false) + mHomeTZ?.setSummary(timezoneName ?: mTimeZoneId) + val tzpd: TimeZonePickerDialog = activity.getFragmentManager() + .findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER) as TimeZonePickerDialog + if (tzpd != null) { + tzpd.setOnTimeZoneSetListener(this) + } + migrateOldPreferences(sharedPreferences) + updateChildPreferences() + } + + private fun showTimezoneDialog() { + val activity: Activity = getActivity() ?: return + val b = Bundle() + b.putLong(TimeZonePickerDialog.BUNDLE_START_TIME_MILLIS, System.currentTimeMillis()) + b.putString(TimeZonePickerDialog.BUNDLE_TIME_ZONE, Utils.getTimeZone(activity, null)) + val fm: FragmentManager = getActivity().getFragmentManager() + var tzpd: TimeZonePickerDialog? = fm + .findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER) as TimeZonePickerDialog + if (tzpd != null) { + tzpd.dismiss() + } + tzpd = TimeZonePickerDialog() + tzpd.setArguments(b) + tzpd.setOnTimeZoneSetListener(this) + tzpd.show(fm, FRAG_TAG_TIME_ZONE_PICKER) + } + + @Override + override fun onStart() { + super.onStart() + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this) + setPreferenceListeners(this) + } + + /** + * Sets up all the preference change listeners to use the specified + * listener. + */ + private fun setPreferenceListeners(listener: OnPreferenceChangeListener?) { + mUseHomeTZ?.setOnPreferenceChangeListener(listener) + mHomeTZ?.setOnPreferenceChangeListener(listener) + mWeekStart?.setOnPreferenceChangeListener(listener) + mDefaultReminder?.setOnPreferenceChangeListener(listener) + mHideDeclined?.setOnPreferenceChangeListener(listener) + mVibrate?.setOnPreferenceChangeListener(listener) + } + + @Override + override fun onStop() { + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this) + setPreferenceListeners(null) + super.onStop() + } + + @Override + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String) { + val a: Activity = getActivity() + if (key.equals(KEY_ALERTS)) { + updateChildPreferences() + if (a != null) { + val intent = Intent() + intent.setClass(a, AlertReceiver::class.java) + if (mAlert?.isChecked() ?: false) { + intent.setAction(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS) + } else { + intent.setAction(AlertReceiver.EVENT_REMINDER_APP_ACTION) + } + a.sendBroadcast(intent) + } + } + if (a != null) { + BackupManager.dataChanged(a.getPackageName()) + } + } + + /** + * Handles time zone preference changes + */ + @Override + override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean { + val tz: String? + val activity: Activity = getActivity() + if (preference === mUseHomeTZ) { + tz = if (newValue != null) { + mTimeZoneId + } else { + CalendarCache.TIMEZONE_TYPE_AUTO + } + Utils.setTimeZone(activity, tz) + return true + } else if (preference === mHideDeclined) { + mHideDeclined?.setChecked(newValue as Boolean) + val intent = Intent(Utils.getWidgetScheduledUpdateAction(activity)) + intent.setDataAndType(CalendarContract.CONTENT_URI, Utils.APPWIDGET_DATA_TYPE) + activity.sendBroadcast(intent) + return true + } else if (preference === mWeekStart) { + mWeekStart?.setValue(newValue as String) + mWeekStart?.setSummary(mWeekStart?.getEntry()) + } else if (preference === mDefaultReminder) { + mDefaultReminder?.setValue(newValue as String) + mDefaultReminder?.setSummary(mDefaultReminder?.getEntry()) + } else if (preference === mVibrate) { + mVibrate?.setChecked(newValue as Boolean) + return true + } else { + return true + } + return false + } + + fun getRingtoneTitleFromUri(context: Context?, uri: String?): String? { + if (TextUtils.isEmpty(uri)) { + return null + } + val ring: Ringtone = RingtoneManager.getRingtone(getActivity(), Uri.parse(uri)) + return if (ring != null) { + ring.getTitle(context) + } else null + } + + /** + * If necessary, upgrades previous versions of preferences to the current + * set of keys and values. + * @param prefs the preferences to upgrade + */ + private fun migrateOldPreferences(prefs: SharedPreferences?) { + // If needed, migrate vibration setting from a previous version + mVibrate?.setChecked(Utils.getDefaultVibrate(getActivity(), prefs)) + + // If needed, migrate the old alerts type settin + if (prefs?.contains(KEY_ALERTS) == false && prefs?.contains(KEY_ALERTS_TYPE) == true) { + val type: String? = prefs?.getString(KEY_ALERTS_TYPE, ALERT_TYPE_STATUS_BAR) + if (type.equals(ALERT_TYPE_OFF)) { + mAlert?.setChecked(false) + mPopup?.setChecked(false) + mPopup?.setEnabled(false) + } else if (type.equals(ALERT_TYPE_STATUS_BAR)) { + mAlert?.setChecked(true) + mPopup?.setChecked(false) + mPopup?.setEnabled(true) + } else if (type.equals(ALERT_TYPE_ALERTS)) { + mAlert?.setChecked(true) + mPopup?.setChecked(true) + mPopup?.setEnabled(true) + } + // clear out the old setting + prefs?.edit().remove(KEY_ALERTS_TYPE).commit() + } + } + + /** + * Keeps the dependent settings in sync with the parent preference, so for + * example, when notifications are turned off, we disable the preferences + * for configuring the exact notification behavior. + */ + private fun updateChildPreferences() { + if (mAlert?.isChecked() ?: false) { + mVibrate?.setEnabled(true) + mPopup?.setEnabled(true) + } else { + mVibrate?.setEnabled(false) + mPopup?.setEnabled(false) + } + } + + @Override + override fun onPreferenceTreeClick( + preferenceScreen: PreferenceScreen?, + preference: Preference + ): Boolean { + val key: String = preference.getKey() + return super.onPreferenceTreeClick(preferenceScreen, preference) + } + + @Override + override fun onTimeZoneSet(tzi: TimeZoneInfo) { + if (mTzPickerUtils == null) { + mTzPickerUtils = TimeZonePickerUtils(getActivity()) + } + val timezoneName: CharSequence? = mTzPickerUtils?.getGmtDisplayName( + getActivity(), tzi.mTzId, System.currentTimeMillis(), false) + mHomeTZ?.setSummary(timezoneName) + Utils.setTimeZone(getActivity(), tzi.mTzId) + } + + companion object { + // The name of the shared preferences file. This name must be maintained for historical + // reasons, as it's what PreferenceManager assigned the first time the file was created. + const val SHARED_PREFS_NAME = "com.android.calendar_preferences" + const val SHARED_PREFS_NAME_NO_BACKUP = "com.android.calendar_preferences_no_backup" + private const val FRAG_TAG_TIME_ZONE_PICKER = "TimeZonePicker" + + // Preference keys + const val KEY_HIDE_DECLINED = "preferences_hide_declined" + const val KEY_WEEK_START_DAY = "preferences_week_start_day" + const val KEY_SHOW_WEEK_NUM = "preferences_show_week_num" + const val KEY_DAYS_PER_WEEK = "preferences_days_per_week" + const val KEY_SKIP_SETUP = "preferences_skip_setup" + const val KEY_CLEAR_SEARCH_HISTORY = "preferences_clear_search_history" + const val KEY_ALERTS_CATEGORY = "preferences_alerts_category" + const val KEY_ALERTS = "preferences_alerts" + const val KEY_ALERTS_VIBRATE = "preferences_alerts_vibrate" + const val KEY_ALERTS_RINGTONE = "preferences_alerts_ringtone" + const val KEY_ALERTS_POPUP = "preferences_alerts_popup" + const val KEY_SHOW_CONTROLS = "preferences_show_controls" + const val KEY_DEFAULT_REMINDER = "preferences_default_reminder" + const val NO_REMINDER = -1 + const val NO_REMINDER_STRING = "-1" + const val REMINDER_DEFAULT_TIME = 10 // in minutes + const val KEY_DEFAULT_CELL_HEIGHT = "preferences_default_cell_height" + const val KEY_VERSION = "preferences_version" + + /** Key to SharePreference for default view (CalendarController.ViewType) */ + const val KEY_START_VIEW = "preferred_startView" + + /** + * Key to SharePreference for default detail view (CalendarController.ViewType) + * Typically used by widget + */ + const val KEY_DETAILED_VIEW = "preferred_detailedView" + const val KEY_DEFAULT_CALENDAR = "preference_defaultCalendar" + + // These must be in sync with the array preferences_week_start_day_values + const val WEEK_START_DEFAULT = "-1" + const val WEEK_START_SATURDAY = "7" + const val WEEK_START_SUNDAY = "1" + const val WEEK_START_MONDAY = "2" + + // These keys are kept to enable migrating users from previous versions + private const val KEY_ALERTS_TYPE = "preferences_alerts_type" + private const val ALERT_TYPE_ALERTS = "0" + private const val ALERT_TYPE_STATUS_BAR = "1" + private const val ALERT_TYPE_OFF = "2" + const val KEY_HOME_TZ_ENABLED = "preferences_home_tz_enabled" + const val KEY_HOME_TZ = "preferences_home_tz" + + // Default preference values + const val DEFAULT_START_VIEW: Int = CalendarController.ViewType.WEEK + const val DEFAULT_DETAILED_VIEW: Int = CalendarController.ViewType.DAY + const val DEFAULT_SHOW_WEEK_NUM = false + + // This should match the XML file. + const val DEFAULT_RINGTONE = "content://settings/system/notification_sound" + + /** Return a properly configured SharedPreferences instance */ + @JvmStatic + fun getSharedPreferences(context: Context?): SharedPreferences? { + return context?.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + } + + /** Set the default shared preferences in the proper context */ + @JvmStatic + fun setDefaultValues(context: Context?) { + PreferenceManager.setDefaultValues(context, SHARED_PREFS_NAME, Context.MODE_PRIVATE, + R.xml.general_preferences, false) + } + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/GoogleCalendarUriIntentFilter.java b/src/com/android/calendar/GoogleCalendarUriIntentFilter.kt index 3970115b..d2fe77f9 100644 --- a/src/com/android/calendar/GoogleCalendarUriIntentFilter.java +++ b/src/com/android/calendar/GoogleCalendarUriIntentFilter.kt @@ -1,6 +1,5 @@ /* -** -** Copyright 2009, The Android Open Source Project +** Copyright 2021, 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. @@ -14,28 +13,25 @@ ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ** limitations under the License. */ +package com.android.calendar -package com.android.calendar; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.os.Bundle; - -public class GoogleCalendarUriIntentFilter extends Activity { - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle - Intent intent = getIntent(); +class GoogleCalendarUriIntentFilter : Activity() { + protected override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + val intent: Intent = getIntent() if (intent != null) { // Pass it on to the next Activity. try { - startNextMatchingActivity(intent); - } catch (ActivityNotFoundException ex) { + startNextMatchingActivity(intent) + } catch (ex: ActivityNotFoundException) { // no browser installed? Just drop it. } } - finish(); + finish() } -} +}
\ No newline at end of file diff --git a/src/com/android/calendar/MultiStateButton.java b/src/com/android/calendar/MultiStateButton.java deleted file mode 100644 index 8034b28e..00000000 --- a/src/com/android/calendar/MultiStateButton.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.widget.Button; - -/** - * <p> - * A button with more than two states. When the button is pressed - * or clicked, the state transitions automatically. - * </p> - * - * <p><strong>XML attributes</strong></p> - * <p> - * See {@link R.styleable#MultiStateButton - * MultiStateButton Attributes}, {@link android.R.styleable#Button Button - * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link - * android.R.styleable#View View Attributes} - * </p> - */ - -public class MultiStateButton extends Button { - //The current state for this button, ranging from 0 to maxState-1 - private int mState; - //The maximum number of states allowed for this button. - private int mMaxStates; - //The currently displaying resource ID. This gets set to a default on creation and remains - //on the last set if the resources get set to null. - private int mButtonResource; - //A list of all drawable resources used by this button in the order it uses them. - private int[] mButtonResources; - private Drawable mButtonDrawable; - - public MultiStateButton(Context context) { - this(context, null); - } - - public MultiStateButton(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public MultiStateButton(Context context, AttributeSet attrs, int defStyle) { - //Currently using the standard buttonStyle, will update when new resources are added. - super(context, attrs, defStyle); - mMaxStates = 1; - mState = 0; - //TODO add a more generic default button - mButtonResources = new int[] { R.drawable.widget_show }; - setButtonDrawable(mButtonResources[mState]); - } - - @Override - public boolean performClick() { - /* When clicked, toggle the state */ - transitionState(); - return super.performClick(); - } - - public void transitionState() { - mState = (mState + 1) % mMaxStates; - setButtonDrawable(mButtonResources[mState]); - } - - /** - * Allows for a new set of drawable resource ids to be set. - * - * This sets the maximum states allowed to the length of the resources array. It will also - * set the current state to the maximum allowed if it's greater than the new max. - */ - public void setButtonResources(int[] resources) throws IllegalArgumentException { - if(resources == null) { - throw new IllegalArgumentException("Button resources cannot be null"); - } - mMaxStates = resources.length; - if(mState >= mMaxStates) { - mState = mMaxStates - 1; - } - mButtonResources = resources; - } - - /** - * Attempts to set the state. Returns true if successful, false otherwise. - */ - public boolean setState(int state){ - if(state >= mMaxStates || state < 0) { - //When moved out of Calendar the tag should be changed. - Log.w("Cal", "MultiStateButton state set to value greater than maxState or < 0"); - return false; - } - mState = state; - setButtonDrawable(mButtonResources[mState]); - return true; - } - - public int getState() { - return mState; - } - - /** - * Set the background to a given Drawable, identified by its resource id. - * - * @param resid the resource id of the drawable to use as the background - */ - public void setButtonDrawable(int resid) { - if (resid != 0 && resid == mButtonResource) { - return; - } - - mButtonResource = resid; - - Drawable d = null; - if (mButtonResource != 0) { - d = getResources().getDrawable(mButtonResource); - } - setButtonDrawable(d); - } - - /** - * Set the background to a given Drawable - * - * @param d The Drawable to use as the background - */ - public void setButtonDrawable(Drawable d) { - if (d != null) { - if (mButtonDrawable != null) { - mButtonDrawable.setCallback(null); - unscheduleDrawable(mButtonDrawable); - } - d.setCallback(this); - d.setState(getDrawableState()); - d.setVisible(getVisibility() == VISIBLE, false); - mButtonDrawable = d; - mButtonDrawable.setState(null); - setMinHeight(mButtonDrawable.getIntrinsicHeight()); - setWidth(mButtonDrawable.getIntrinsicWidth()); - } - refreshDrawableState(); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (mButtonDrawable != null) { - final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; - final int horizontalGravity = getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK; - final int height = mButtonDrawable.getIntrinsicHeight(); - final int width = mButtonDrawable.getIntrinsicWidth(); - - int y = 0; - int x = 0; - - switch (verticalGravity) { - case Gravity.BOTTOM: - y = getHeight() - height; - break; - case Gravity.CENTER_VERTICAL: - y = (getHeight() - height) / 2; - break; - } - switch (horizontalGravity) { - case Gravity.RIGHT: - x = getWidth() - width; - break; - case Gravity.CENTER_HORIZONTAL: - x = (getWidth() - width) / 2; - break; - } - - mButtonDrawable.setBounds(x, y, x + width, y + height); - mButtonDrawable.draw(canvas); - } - } -} diff --git a/src/com/android/calendar/MultiStateButton.kt b/src/com/android/calendar/MultiStateButton.kt new file mode 100644 index 00000000..f86ee6ba --- /dev/null +++ b/src/com/android/calendar/MultiStateButton.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.util.Log +import android.view.Gravity +import android.widget.Button + +/** + * A button with more than two states. When the button is pressed + * or clicked, the state transitions automatically. + * + * **XML attributes** + * See [ MultiStateButton Attributes][R.styleable.MultiStateButton], + * [Button][android.R.styleable.Button], [TextView Attributes][android.R.styleable.TextView], + * [ ][android.R.styleable.View] + * + */ +class MultiStateButton(context: Context?, attrs: AttributeSet?, defStyle: Int) : + Button(context, attrs, defStyle) { + //The current state for this button, ranging from 0 to maxState-1 + var mState = 0 + private set + + //The maximum number of states allowed for this button. + private var mMaxStates = 1 + + //The currently displaying resource ID. This gets set to a default on creation and remains + //on the last set if the resources get set to null. + private var mButtonResource = 0 + + //A list of all drawable resources used by this button in the order it uses them. + private var mButtonResources: IntArray + private var mButtonDrawable: Drawable? = null + + constructor(context: Context?) : this(context, null) {} + constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) {} + + override fun performClick(): Boolean { + /* When clicked, toggle the state */ + transitionState() + return super.performClick() + } + + fun transitionState() { + mState = (mState + 1) % mMaxStates + setButtonDrawable(mButtonResources[mState]) + } + + /** + * Allows for a new set of drawable resource ids to be set. + * + * This sets the maximum states allowed to the length of the resources array. It will also + * set the current state to the maximum allowed if it's greater than the new max. + */ + //@Throws(IllegalArgumentException::class) + fun setButtonResources(resources: IntArray?) { + if (resources == null) { + throw IllegalArgumentException("Button resources cannot be null") + } + mMaxStates = resources.size + if (mState >= mMaxStates) { + mState = mMaxStates - 1 + } + mButtonResources = resources + } + + /** + * Attempts to set the state. Returns true if successful, false otherwise. + */ + fun setState(state: Int): Boolean { + if (state >= mMaxStates || state < 0) { + //When moved out of Calendar the tag should be changed. + Log.w("Cal", "MultiStateButton state set to value greater than maxState or < 0") + return false + } + mState = state + setButtonDrawable(mButtonResources[mState]) + return true + } + + /** + * Set the background to a given Drawable, identified by its resource id. + * + * @param resid the resource id of the drawable to use as the background + */ + fun setButtonDrawable(resid: Int) { + if (resid != 0 && resid == mButtonResource) { + return + } + mButtonResource = resid + var d: Drawable? = null + if (mButtonResource != 0) { + d = getResources().getDrawable(mButtonResource) + } + setButtonDrawable(d) + } + + /** + * Set the background to a given Drawable + * + * @param d The Drawable to use as the background + */ + fun setButtonDrawable(d: Drawable?) { + if (d != null) { + if (mButtonDrawable != null) { + mButtonDrawable?.setCallback(null) + unscheduleDrawable(mButtonDrawable) + } + d.setCallback(this) + d.setState(getDrawableState()) + d.setVisible(getVisibility() === VISIBLE, false) + mButtonDrawable = d + mButtonDrawable?.setState(getDrawableState()) + setMinHeight(mButtonDrawable?.getIntrinsicHeight() ?: 0) + setWidth(mButtonDrawable?.getIntrinsicWidth() ?: 0) + } + refreshDrawableState() + } + + protected override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (mButtonDrawable != null) { + val verticalGravity: Int = getGravity() and Gravity.VERTICAL_GRAVITY_MASK + val horizontalGravity: Int = getGravity() and Gravity.HORIZONTAL_GRAVITY_MASK + val height: Int = mButtonDrawable?.getIntrinsicHeight() ?: 0 + val width: Int = mButtonDrawable?.getIntrinsicWidth() ?: 0 + var y = 0 + var x = 0 + when (verticalGravity) { + Gravity.BOTTOM -> y = getHeight() - height + Gravity.CENTER_VERTICAL -> y = (getHeight() - height) / 2 + } + when (horizontalGravity) { + Gravity.RIGHT -> x = getWidth() - width + Gravity.CENTER_HORIZONTAL -> x = (getWidth() - width) / 2 + } + mButtonDrawable?.setBounds(x, y, x + width, y + height) + mButtonDrawable?.draw(canvas) + } + } + + init { + //Currently using the standard buttonStyle, will update when new resources are added. + //TODO add a more generic default button + mButtonResources = intArrayOf(R.drawable.widget_show) + setButtonDrawable(mButtonResources[mState]) + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/OtherPreferences.java b/src/com/android/calendar/OtherPreferences.java deleted file mode 100644 index a59d3f46..00000000 --- a/src/com/android/calendar/OtherPreferences.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright (C) 2011 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.calendar; - -import android.app.Activity; -import android.app.Dialog; -import android.app.TimePickerDialog; -import android.content.ComponentName; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; -import android.preference.PreferenceScreen; -import android.text.format.DateFormat; -import android.text.format.Time; -import android.util.Log; -import android.widget.TimePicker; - -public class OtherPreferences extends PreferenceFragment implements OnPreferenceChangeListener{ - private static final String TAG = "CalendarOtherPreferences"; - - // The name of the shared preferences file. This name must be maintained for - // historical reasons, as it's what PreferenceManager assigned the first - // time the file was created. - static final String SHARED_PREFS_NAME = "com.android.calendar_preferences"; - - // Must be the same keys that are used in the other_preferences.xml file. - public static final String KEY_OTHER_COPY_DB = "preferences_copy_db"; - public static final String KEY_OTHER_QUIET_HOURS = "preferences_reminders_quiet_hours"; - public static final String KEY_OTHER_REMINDERS_RESPONDED = "preferences_reminders_responded"; - public static final String KEY_OTHER_QUIET_HOURS_START = - "preferences_reminders_quiet_hours_start"; - public static final String KEY_OTHER_QUIET_HOURS_START_HOUR = - "preferences_reminders_quiet_hours_start_hour"; - public static final String KEY_OTHER_QUIET_HOURS_START_MINUTE = - "preferences_reminders_quiet_hours_start_minute"; - public static final String KEY_OTHER_QUIET_HOURS_END = - "preferences_reminders_quiet_hours_end"; - public static final String KEY_OTHER_QUIET_HOURS_END_HOUR = - "preferences_reminders_quiet_hours_end_hour"; - public static final String KEY_OTHER_QUIET_HOURS_END_MINUTE = - "preferences_reminders_quiet_hours_end_minute"; - public static final String KEY_OTHER_1 = "preferences_tardis_1"; - - public static final int QUIET_HOURS_DEFAULT_START_HOUR = 22; - public static final int QUIET_HOURS_DEFAULT_START_MINUTE = 0; - public static final int QUIET_HOURS_DEFAULT_END_HOUR = 8; - public static final int QUIET_HOURS_DEFAULT_END_MINUTE = 0; - - private static final int START_LISTENER = 1; - private static final int END_LISTENER = 2; - private static final String format24Hour = "%H:%M"; - private static final String format12Hour = "%I:%M%P"; - - private Preference mCopyDb; - private CheckBoxPreference mQuietHours; - private Preference mQuietHoursStart; - private Preference mQuietHoursEnd; - - private TimePickerDialog mTimePickerDialog; - private TimeSetListener mQuietHoursStartListener; - private TimePickerDialog mQuietHoursStartDialog; - private TimeSetListener mQuietHoursEndListener; - private TimePickerDialog mQuietHoursEndDialog; - private boolean mIs24HourMode; - - public OtherPreferences() { - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - PreferenceManager manager = getPreferenceManager(); - manager.setSharedPreferencesName(SHARED_PREFS_NAME); - SharedPreferences prefs = manager.getSharedPreferences(); - - addPreferencesFromResource(R.xml.other_preferences); - mCopyDb = findPreference(KEY_OTHER_COPY_DB); - - Activity activity = getActivity(); - if (activity == null) { - Log.d(TAG, "Activity was null"); - } - mIs24HourMode = DateFormat.is24HourFormat(activity); - - mQuietHours = - (CheckBoxPreference) findPreference(KEY_OTHER_QUIET_HOURS); - - int startHour = prefs.getInt(KEY_OTHER_QUIET_HOURS_START_HOUR, - QUIET_HOURS_DEFAULT_START_HOUR); - int startMinute = prefs.getInt(KEY_OTHER_QUIET_HOURS_START_MINUTE, - QUIET_HOURS_DEFAULT_START_MINUTE); - mQuietHoursStart = findPreference(KEY_OTHER_QUIET_HOURS_START); - mQuietHoursStartListener = new TimeSetListener(START_LISTENER); - mQuietHoursStartDialog = new TimePickerDialog( - activity, mQuietHoursStartListener, - startHour, startMinute, mIs24HourMode); - mQuietHoursStart.setSummary(formatTime(startHour, startMinute)); - - int endHour = prefs.getInt(KEY_OTHER_QUIET_HOURS_END_HOUR, - QUIET_HOURS_DEFAULT_END_HOUR); - int endMinute = prefs.getInt(KEY_OTHER_QUIET_HOURS_END_MINUTE, - QUIET_HOURS_DEFAULT_END_MINUTE); - mQuietHoursEnd = findPreference(KEY_OTHER_QUIET_HOURS_END); - mQuietHoursEndListener = new TimeSetListener(END_LISTENER); - mQuietHoursEndDialog = new TimePickerDialog( - activity, mQuietHoursEndListener, - endHour, endMinute, mIs24HourMode); - mQuietHoursEnd.setSummary(formatTime(endHour, endMinute)); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object objValue) { - return true; - } - - @Override - public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { - if (preference == mCopyDb) { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setComponent(new ComponentName("com.android.providers.calendar", - "com.android.providers.calendar.CalendarDebugActivity")); - startActivity(intent); - } else if (preference == mQuietHoursStart) { - if (mTimePickerDialog == null) { - mTimePickerDialog = mQuietHoursStartDialog; - mTimePickerDialog.show(); - } else { - Log.v(TAG, "not null"); - } - } else if (preference == mQuietHoursEnd) { - if (mTimePickerDialog == null) { - mTimePickerDialog = mQuietHoursEndDialog; - mTimePickerDialog.show(); - } else { - Log.v(TAG, "not null"); - } - } else { - return super.onPreferenceTreeClick(screen, preference); - } - return true; - } - - private class TimeSetListener implements TimePickerDialog.OnTimeSetListener { - private int mListenerId; - - public TimeSetListener(int listenerId) { - mListenerId = listenerId; - } - - @Override - public void onTimeSet(TimePicker view, int hourOfDay, int minute) { - mTimePickerDialog = null; - - SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); - SharedPreferences.Editor editor = prefs.edit(); - - String summary = formatTime(hourOfDay, minute); - switch (mListenerId) { - case (START_LISTENER): - mQuietHoursStart.setSummary(summary); - editor.putInt(KEY_OTHER_QUIET_HOURS_START_HOUR, hourOfDay); - editor.putInt(KEY_OTHER_QUIET_HOURS_START_MINUTE, minute); - break; - case (END_LISTENER): - mQuietHoursEnd.setSummary(summary); - editor.putInt(KEY_OTHER_QUIET_HOURS_END_HOUR, hourOfDay); - editor.putInt(KEY_OTHER_QUIET_HOURS_END_MINUTE, minute); - break; - default: - Log.d(TAG, "Set time for unknown listener: "+mListenerId); - } - - editor.commit(); - } - } - - /** - * @param hourOfDay the hour of the day (0-24) - * @param minute - * @return human-readable string formatted based on 24-hour mode. - */ - private String formatTime(int hourOfDay, int minute) { - Time time = new Time(); - time.hour = hourOfDay; - time.minute = minute; - - String format = mIs24HourMode? format24Hour : format12Hour; - return time.format(format); - } -} diff --git a/src/com/android/calendar/OtherPreferences.kt b/src/com/android/calendar/OtherPreferences.kt new file mode 100644 index 00000000..f1507ccf --- /dev/null +++ b/src/com/android/calendar/OtherPreferences.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.app.Activity +import android.app.Dialog +import android.app.TimePickerDialog +import android.content.ComponentName +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.preference.CheckBoxPreference +import android.preference.ListPreference +import android.preference.Preference +import android.preference.Preference.OnPreferenceChangeListener +import android.preference.PreferenceFragment +import android.preference.PreferenceManager +import android.preference.PreferenceScreen +import android.text.format.DateFormat +import android.text.format.Time +import android.util.Log +import android.widget.TimePicker + +class OtherPreferences : PreferenceFragment(), OnPreferenceChangeListener { + private var mCopyDb: Preference? = null + private var mQuietHours: CheckBoxPreference? = null + private var mQuietHoursStart: Preference? = null + private var mQuietHoursEnd: Preference? = null + private var mTimePickerDialog: TimePickerDialog? = null + private var mQuietHoursStartListener: TimeSetListener? = null + private var mQuietHoursStartDialog: TimePickerDialog? = null + private var mQuietHoursEndListener: TimeSetListener? = null + private var mQuietHoursEndDialog: TimePickerDialog? = null + private var mIs24HourMode = false + + @Override + override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + val manager: PreferenceManager = getPreferenceManager() + manager.setSharedPreferencesName(SHARED_PREFS_NAME) + val prefs: SharedPreferences = manager.getSharedPreferences() + addPreferencesFromResource(R.xml.other_preferences) + mCopyDb = findPreference(KEY_OTHER_COPY_DB) + val activity: Activity = getActivity() + if (activity == null) { + Log.d(TAG, "Activity was null") + } + mIs24HourMode = DateFormat.is24HourFormat(activity) + mQuietHours = findPreference(KEY_OTHER_QUIET_HOURS) as CheckBoxPreference? + val startHour: Int = prefs.getInt(KEY_OTHER_QUIET_HOURS_START_HOUR, + QUIET_HOURS_DEFAULT_START_HOUR) + val startMinute: Int = prefs.getInt(KEY_OTHER_QUIET_HOURS_START_MINUTE, + QUIET_HOURS_DEFAULT_START_MINUTE) + mQuietHoursStart = findPreference(KEY_OTHER_QUIET_HOURS_START) + mQuietHoursStartListener = TimeSetListener(START_LISTENER) + mQuietHoursStartDialog = TimePickerDialog( + activity, mQuietHoursStartListener, + startHour, startMinute, mIs24HourMode) + mQuietHoursStart?.setSummary(formatTime(startHour, startMinute)) + val endHour: Int = prefs.getInt(KEY_OTHER_QUIET_HOURS_END_HOUR, + QUIET_HOURS_DEFAULT_END_HOUR) + val endMinute: Int = prefs.getInt(KEY_OTHER_QUIET_HOURS_END_MINUTE, + QUIET_HOURS_DEFAULT_END_MINUTE) + mQuietHoursEnd = findPreference(KEY_OTHER_QUIET_HOURS_END) + mQuietHoursEndListener = TimeSetListener(END_LISTENER) + mQuietHoursEndDialog = TimePickerDialog( + activity, mQuietHoursEndListener, + endHour, endMinute, mIs24HourMode) + mQuietHoursEnd?.setSummary(formatTime(endHour, endMinute)) + } + + @Override + override fun onPreferenceChange(preference: Preference?, objValue: Any?): Boolean { + return true + } + + @Override + override fun onPreferenceTreeClick(screen: PreferenceScreen?, preference: Preference): Boolean { + if (preference === mCopyDb) { + val intent = Intent(Intent.ACTION_MAIN) + intent.setComponent(ComponentName("com.android.providers.calendar", + "com.android.providers.calendar.CalendarDebugActivity")) + startActivity(intent) + } else if (preference === mQuietHoursStart) { + if (mTimePickerDialog == null) { + mTimePickerDialog = mQuietHoursStartDialog + mTimePickerDialog?.show() + } else { + Log.v(TAG, "not null") + } + } else if (preference === mQuietHoursEnd) { + if (mTimePickerDialog == null) { + mTimePickerDialog = mQuietHoursEndDialog + mTimePickerDialog?.show() + } else { + Log.v(TAG, "not null") + } + } else { + return super.onPreferenceTreeClick(screen, preference) + } + return true + } + + private inner class TimeSetListener(private val mListenerId: Int) : + TimePickerDialog.OnTimeSetListener { + @Override + override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) { + mTimePickerDialog = null + val prefs: SharedPreferences = getPreferenceManager().getSharedPreferences() + val editor: SharedPreferences.Editor = prefs.edit() + val summary = formatTime(hourOfDay, minute) + when (mListenerId) { + START_LISTENER -> { + mQuietHoursStart?.setSummary(summary) + editor.putInt(KEY_OTHER_QUIET_HOURS_START_HOUR, hourOfDay) + editor.putInt(KEY_OTHER_QUIET_HOURS_START_MINUTE, minute) + } + END_LISTENER -> { + mQuietHoursEnd?.setSummary(summary) + editor.putInt(KEY_OTHER_QUIET_HOURS_END_HOUR, hourOfDay) + editor.putInt(KEY_OTHER_QUIET_HOURS_END_MINUTE, minute) + } + else -> Log.d(TAG, "Set time for unknown listener: $mListenerId") + } + editor.commit() + } + } + + /** + * @param hourOfDay the hour of the day (0-24) + * @param minute + * @return human-readable string formatted based on 24-hour mode. + */ + private fun formatTime(hourOfDay: Int, minute: Int): String { + val time = Time() + time.hour = hourOfDay + time.minute = minute + val format = if (mIs24HourMode) format24Hour else format12Hour + return time.format(format) + } + + companion object { + private const val TAG = "CalendarOtherPreferences" + + // The name of the shared preferences file. This name must be maintained for + // historical reasons, as it's what PreferenceManager assigned the first + // time the file was created. + const val SHARED_PREFS_NAME = "com.android.calendar_preferences" + + // Must be the same keys that are used in the other_preferences.xml file. + const val KEY_OTHER_COPY_DB = "preferences_copy_db" + const val KEY_OTHER_QUIET_HOURS = "preferences_reminders_quiet_hours" + const val KEY_OTHER_REMINDERS_RESPONDED = "preferences_reminders_responded" + const val KEY_OTHER_QUIET_HOURS_START = "preferences_reminders_quiet_hours_start" + const val KEY_OTHER_QUIET_HOURS_START_HOUR = "preferences_reminders_quiet_hours_start_hour" + const val KEY_OTHER_QUIET_HOURS_START_MINUTE = + "preferences_reminders_quiet_hours_start_minute" + const val KEY_OTHER_QUIET_HOURS_END = "preferences_reminders_quiet_hours_end" + const val KEY_OTHER_QUIET_HOURS_END_HOUR = "preferences_reminders_quiet_hours_end_hour" + const val KEY_OTHER_QUIET_HOURS_END_MINUTE = "preferences_reminders_quiet_hours_end_minute" + const val KEY_OTHER_1 = "preferences_tardis_1" + const val QUIET_HOURS_DEFAULT_START_HOUR = 22 + const val QUIET_HOURS_DEFAULT_START_MINUTE = 0 + const val QUIET_HOURS_DEFAULT_END_HOUR = 8 + const val QUIET_HOURS_DEFAULT_END_MINUTE = 0 + private const val START_LISTENER = 1 + private const val END_LISTENER = 2 + private const val format24Hour = "%H:%M" + private const val format12Hour = "%I:%M%P" + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/StickyHeaderListView.java b/src/com/android/calendar/StickyHeaderListView.java deleted file mode 100644 index 981e7af7..00000000 --- a/src/com/android/calendar/StickyHeaderListView.java +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Copyright (C) 2011 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.calendar; - -import android.content.Context; -import android.graphics.Color; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.Adapter; -import android.widget.FrameLayout; -import android.widget.ListView; - -/** - * Implements a ListView class with a sticky header at the top. The header is - * per section and it is pinned to the top as long as its section is at the top - * of the view. If it is not, the header slides up or down (depending on the - * scroll movement) and the header of the current section slides to the top. - * Notes: - * 1. The class uses the first available child ListView as the working - * ListView. If no ListView child exists, the class will create a default one. - * 2. The ListView's adapter must be passed to this class using the 'setAdapter' - * method. The adapter must implement the HeaderIndexer interface. If no adapter - * is specified, the class will try to extract it from the ListView - * 3. The class registers itself as a listener to scroll events (OnScrollListener), if the - * ListView needs to receive scroll events, it must register its listener using - * this class' setOnScrollListener method. - * 4. Headers for the list view must be added before using the StickyHeaderListView - * 5. The implementation should register to listen to dataset changes. Right now this is not done - * since a change the dataset in a listview forces a call to OnScroll. The needed code is - * commented out. - */ -public class StickyHeaderListView extends FrameLayout implements OnScrollListener { - - private static final String TAG = "StickyHeaderListView"; - protected boolean mChildViewsCreated = false; - protected boolean mDoHeaderReset = false; - - protected Context mContext = null; - protected Adapter mAdapter = null; - protected HeaderIndexer mIndexer = null; - protected HeaderHeightListener mHeaderHeightListener = null; - protected View mStickyHeader = null; - protected View mNonessentialHeader = null; // A invisible header used when a section has no header - protected ListView mListView = null; - protected ListView.OnScrollListener mListener = null; - - private int mSeparatorWidth; - private View mSeparatorView; - private int mLastStickyHeaderHeight = 0; - - // This code is needed only if dataset changes do not force a call to OnScroll - // protected DataSetObserver mListDataObserver = null; - - - protected int mCurrentSectionPos = -1; // Position of section that has its header on the - // top of the view - protected int mNextSectionPosition = -1; // Position of next section's header - protected int mListViewHeadersCount = 0; - - /** - * Interface that must be implemented by the ListView adapter to provide headers locations - * and number of items under each header. - * - */ - public interface HeaderIndexer { - /** - * Calculates the position of the header of a specific item in the adapter's data set. - * For example: Assuming you have a list with albums and songs names: - * Album A, song 1, song 2, ...., song 10, Album B, song 1, ..., song 7. A call to - * this method with the position of song 5 in Album B, should return the position - * of Album B. - * @param position - Position of the item in the ListView dataset - * @return Position of header. -1 if the is no header - */ - - int getHeaderPositionFromItemPosition(int position); - - /** - * Calculates the number of items in the section defined by the header (not including - * the header). - * For example: A list with albums and songs, the method should return - * the number of songs names (without the album name). - * - * @param headerPosition - the value returned by 'getHeaderPositionFromItemPosition' - * @return Number of items. -1 on error. - */ - int getHeaderItemsNumber(int headerPosition); - } - - /*** - * - * Interface that is used to update the sticky header's height - * - */ - public interface HeaderHeightListener { - - /*** - * Updated a change in the sticky header's size - * - * @param height - new height of sticky header - */ - void OnHeaderHeightChanged(int height); - } - - /** - * Sets the adapter to be used by the class to get views of headers - * - * @param adapter - The adapter. - */ - - public void setAdapter(Adapter adapter) { - - // This code is needed only if dataset changes do not force a call to - // OnScroll - // if (mAdapter != null && mListDataObserver != null) { - // mAdapter.unregisterDataSetObserver(mListDataObserver); - // } - - if (adapter != null) { - mAdapter = adapter; - // This code is needed only if dataset changes do not force a call - // to OnScroll - // mAdapter.registerDataSetObserver(mListDataObserver); - } - } - - /** - * Sets the indexer object (that implements the HeaderIndexer interface). - * - * @param indexer - The indexer. - */ - - public void setIndexer(HeaderIndexer indexer) { - mIndexer = indexer; - } - - /** - * Sets the list view that is displayed - * @param lv - The list view. - */ - - public void setListView(ListView lv) { - mListView = lv; - mListView.setOnScrollListener(this); - mListViewHeadersCount = mListView.getHeaderViewsCount(); - } - - /** - * Sets an external OnScroll listener. Since the StickyHeaderListView sets - * itself as the scroll events listener of the listview, this method allows - * the user to register another listener that will be called after this - * class listener is called. - * - * @param listener - The external listener. - */ - public void setOnScrollListener(ListView.OnScrollListener listener) { - mListener = listener; - } - - public void setHeaderHeightListener(HeaderHeightListener listener) { - mHeaderHeightListener = listener; - } - - // This code is needed only if dataset changes do not force a call to OnScroll - // protected void createDataListener() { - // mListDataObserver = new DataSetObserver() { - // @Override - // public void onChanged() { - // onDataChanged(); - // } - // }; - // } - - /** - * Constructor - * - * @param context - application context. - * @param attrs - layout attributes. - */ - public StickyHeaderListView(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - // This code is needed only if dataset changes do not force a call to OnScroll - // createDataListener(); - } - - /** - * Scroll status changes listener - * - * @param view - the scrolled view - * @param scrollState - new scroll state. - */ - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - if (mListener != null) { - mListener.onScrollStateChanged(view, scrollState); - } - } - - /** - * Scroll events listener - * - * @param view - the scrolled view - * @param firstVisibleItem - the index (in the list's adapter) of the top - * visible item. - * @param visibleItemCount - the number of visible items in the list - * @param totalItemCount - the total number items in the list - */ - @Override - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - - updateStickyHeader(firstVisibleItem); - - if (mListener != null) { - mListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); - } - } - - /** - * Sets a separator below the sticky header, which will be visible while the sticky header - * is not scrolling up. - * @param color - color of separator - * @param width - width in pixels of separator - */ - public void setHeaderSeparator(int color, int width) { - mSeparatorView = new View(mContext); - ViewGroup.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, - width, Gravity.TOP); - mSeparatorView.setLayoutParams(params); - mSeparatorView.setBackgroundColor(color); - mSeparatorWidth = width; - this.addView(mSeparatorView); - } - - protected void updateStickyHeader(int firstVisibleItem) { - - // Try to make sure we have an adapter to work with (may not succeed). - if (mAdapter == null && mListView != null) { - setAdapter(mListView.getAdapter()); - } - - firstVisibleItem -= mListViewHeadersCount; - if (mAdapter != null && mIndexer != null && mDoHeaderReset) { - - // Get the section header position - int sectionSize = 0; - int sectionPos = mIndexer.getHeaderPositionFromItemPosition(firstVisibleItem); - - // New section - set it in the header view - boolean newView = false; - if (sectionPos != mCurrentSectionPos) { - - // No header for current position , use the nonessential invisible one, hide the separator - if (sectionPos == -1) { - sectionSize = 0; - this.removeView(mStickyHeader); - mStickyHeader = mNonessentialHeader; - if (mSeparatorView != null) { - mSeparatorView.setVisibility(View.GONE); - } - newView = true; - } else { - // Create a copy of the header view to show on top - sectionSize = mIndexer.getHeaderItemsNumber(sectionPos); - View v = mAdapter.getView(sectionPos + mListViewHeadersCount, null, mListView); - v.measure(MeasureSpec.makeMeasureSpec(mListView.getWidth(), - MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mListView.getHeight(), - MeasureSpec.AT_MOST)); - this.removeView(mStickyHeader); - mStickyHeader = v; - newView = true; - } - mCurrentSectionPos = sectionPos; - mNextSectionPosition = sectionSize + sectionPos + 1; - } - - - // Do transitions - // If position of bottom of last item in a section is smaller than the height of the - // sticky header - shift drawable of header. - if (mStickyHeader != null) { - int sectionLastItemPosition = mNextSectionPosition - firstVisibleItem - 1; - int stickyHeaderHeight = mStickyHeader.getHeight(); - if (stickyHeaderHeight == 0) { - stickyHeaderHeight = mStickyHeader.getMeasuredHeight(); - } - - // Update new header height - if (mHeaderHeightListener != null && - mLastStickyHeaderHeight != stickyHeaderHeight) { - mLastStickyHeaderHeight = stickyHeaderHeight; - mHeaderHeightListener.OnHeaderHeightChanged(stickyHeaderHeight); - } - - View SectionLastView = mListView.getChildAt(sectionLastItemPosition); - if (SectionLastView != null && SectionLastView.getBottom() <= stickyHeaderHeight) { - int lastViewBottom = SectionLastView.getBottom(); - mStickyHeader.setTranslationY(lastViewBottom - stickyHeaderHeight); - if (mSeparatorView != null) { - mSeparatorView.setVisibility(View.GONE); - } - } else if (stickyHeaderHeight != 0) { - mStickyHeader.setTranslationY(0); - if (mSeparatorView != null && !mStickyHeader.equals(mNonessentialHeader)) { - mSeparatorView.setVisibility(View.VISIBLE); - } - } - if (newView) { - mStickyHeader.setVisibility(View.INVISIBLE); - this.addView(mStickyHeader); - if (mSeparatorView != null && !mStickyHeader.equals(mNonessentialHeader)){ - FrameLayout.LayoutParams params = - new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, - mSeparatorWidth); - params.setMargins(0, mStickyHeader.getMeasuredHeight(), 0, 0); - mSeparatorView.setLayoutParams(params); - mSeparatorView.setVisibility(View.VISIBLE); - } - mStickyHeader.setVisibility(View.VISIBLE); - } - } - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - if (!mChildViewsCreated) { - setChildViews(); - } - mDoHeaderReset = true; - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - if (!mChildViewsCreated) { - setChildViews(); - } - mDoHeaderReset = true; - } - - - // Resets the sticky header when the adapter data set was changed - // This code is needed only if dataset changes do not force a call to OnScroll - // protected void onDataChanged() { - // Should do a call to updateStickyHeader if needed - // } - - private void setChildViews() { - - // Find a child ListView (if any) - int iChildNum = getChildCount(); - for (int i = 0; i < iChildNum; i++) { - Object v = getChildAt(i); - if (v instanceof ListView) { - setListView((ListView) v); - } - } - - // No child ListView - add one - if (mListView == null) { - setListView(new ListView(mContext)); - } - - // Create a nonessential view , it will be used in case a section has no header - mNonessentialHeader = new View (mContext); - ViewGroup.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, - 1, Gravity.TOP); - mNonessentialHeader.setLayoutParams(params); - mNonessentialHeader.setBackgroundColor(Color.TRANSPARENT); - - mChildViewsCreated = true; - } - -} diff --git a/src/com/android/calendar/StickyHeaderListView.kt b/src/com/android/calendar/StickyHeaderListView.kt new file mode 100644 index 00000000..37733b7b --- /dev/null +++ b/src/com/android/calendar/StickyHeaderListView.kt @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.AbsListView +import android.widget.AbsListView.OnScrollListener +import android.widget.Adapter +import android.widget.FrameLayout +import android.widget.ListView + +/** + * Implements a ListView class with a sticky header at the top. The header is + * per section and it is pinned to the top as long as its section is at the top + * of the view. If it is not, the header slides up or down (depending on the + * scroll movement) and the header of the current section slides to the top. + * Notes: + * 1. The class uses the first available child ListView as the working + * ListView. If no ListView child exists, the class will create a default one. + * 2. The ListView's adapter must be passed to this class using the 'setAdapter' + * method. The adapter must implement the HeaderIndexer interface. If no adapter + * is specified, the class will try to extract it from the ListView + * 3. The class registers itself as a listener to scroll events (OnScrollListener), if the + * ListView needs to receive scroll events, it must register its listener using + * this class' setOnScrollListener method. + * 4. Headers for the list view must be added before using the StickyHeaderListView + * 5. The implementation should register to listen to dataset changes. Right now this is not done + * since a change the dataset in a listview forces a call to OnScroll. The needed code is + * commented out. + */ +class StickyHeaderListView(context: Context, attrs: AttributeSet?) : + FrameLayout(context, attrs), OnScrollListener { + protected var mChildViewsCreated = false + protected var mDoHeaderReset = false + protected var mContext: Context? = null + protected var mAdapter: Adapter? = null + protected var mIndexer: HeaderIndexer? = null + protected var mHeaderHeightListener: HeaderHeightListener? = null + protected var mStickyHeader: View? = null + // A invisible header used when a section has no header + protected var mNonessentialHeader: View? = null + protected var mListView: ListView? = null + protected var mListener: AbsListView.OnScrollListener? = null + private var mSeparatorWidth = 0 + private var mSeparatorView: View? = null + private var mLastStickyHeaderHeight = 0 + + // This code is needed only if dataset changes do not force a call to OnScroll + // protected DataSetObserver mListDataObserver = null; + protected var mCurrentSectionPos = -1 // Position of section that has its header on the + + // top of the view + protected var mNextSectionPosition = -1 // Position of next section's header + protected var mListViewHeadersCount = 0 + + /** + * Interface that must be implemented by the ListView adapter to provide headers locations + * and number of items under each header. + * + */ + interface HeaderIndexer { + /** + * Calculates the position of the header of a specific item in the adapter's data set. + * For example: Assuming you have a list with albums and songs names: + * Album A, song 1, song 2, ...., song 10, Album B, song 1, ..., song 7. A call to + * this method with the position of song 5 in Album B, should return the position + * of Album B. + * @param position - Position of the item in the ListView dataset + * @return Position of header. -1 if the is no header + */ + fun getHeaderPositionFromItemPosition(position: Int): Int + + /** + * Calculates the number of items in the section defined by the header (not including + * the header). + * For example: A list with albums and songs, the method should return + * the number of songs names (without the album name). + * + * @param headerPosition - the value returned by 'getHeaderPositionFromItemPosition' + * @return Number of items. -1 on error. + */ + fun getHeaderItemsNumber(headerPosition: Int): Int + } + + /*** + * + * Interface that is used to update the sticky header's height + * + */ + interface HeaderHeightListener { + /*** + * Updated a change in the sticky header's size + * + * @param height - new height of sticky header + */ + fun OnHeaderHeightChanged(height: Int) + } + + /** + * Sets the adapter to be used by the class to get views of headers + * + * @param adapter - The adapter. + */ + fun setAdapter(adapter: Adapter?) { + // This code is needed only if dataset changes do not force a call to + // OnScroll + // if (mAdapter != null && mListDataObserver != null) { + // mAdapter.unregisterDataSetObserver(mListDataObserver); + // } + if (adapter != null) { + mAdapter = adapter + // This code is needed only if dataset changes do not force a call + // to OnScroll + // mAdapter.registerDataSetObserver(mListDataObserver); + } + } + + /** + * Sets the indexer object (that implements the HeaderIndexer interface). + * + * @param indexer - The indexer. + */ + fun setIndexer(indexer: HeaderIndexer?) { + mIndexer = indexer + } + + /** + * Sets the list view that is displayed + * @param lv - The list view. + */ + fun setListView(lv: ListView?) { + mListView = lv + mListView?.setOnScrollListener(this) + mListViewHeadersCount = mListView?.getHeaderViewsCount() as Int + } + + /** + * Sets an external OnScroll listener. Since the StickyHeaderListView sets + * itself as the scroll events listener of the listview, this method allows + * the user to register another listener that will be called after this + * class listener is called. + * + * @param listener - The external listener. + */ + fun setOnScrollListener(listener: AbsListView.OnScrollListener?) { + mListener = listener + } + + fun setHeaderHeightListener(listener: HeaderHeightListener?) { + mHeaderHeightListener = listener + } + + /** + * Scroll status changes listener + * + * @param view - the scrolled view + * @param scrollState - new scroll state. + */ + @Override + override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) { + if (mListener != null) { + mListener?.onScrollStateChanged(view, scrollState) + } + } + + /** + * Scroll events listener + * + * @param view - the scrolled view + * @param firstVisibleItem - the index (in the list's adapter) of the top + * visible item. + * @param visibleItemCount - the number of visible items in the list + * @param totalItemCount - the total number items in the list + */ + @Override + override fun onScroll( + view: AbsListView?, + firstVisibleItem: Int, + visibleItemCount: Int, + totalItemCount: Int + ) { + updateStickyHeader(firstVisibleItem) + if (mListener != null) { + mListener?.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount) + } + } + + /** + * Sets a separator below the sticky header, which will be visible while the sticky header + * is not scrolling up. + * @param color - color of separator + * @param width - width in pixels of separator + */ + fun setHeaderSeparator(color: Int, width: Int) { + mSeparatorView = View(mContext) + val params: ViewGroup.LayoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + width, Gravity.TOP + ) + mSeparatorView?.setLayoutParams(params) + mSeparatorView?.setBackgroundColor(color) + mSeparatorWidth = width + this.addView(mSeparatorView) + } + + protected fun updateStickyHeader(firstVisibleItemInput: Int) { + // Try to make sure we have an adapter to work with (may not succeed). + var firstVisibleItem = firstVisibleItemInput + if (mAdapter == null && mListView != null) { + setAdapter(mListView?.getAdapter()) + } + firstVisibleItem -= mListViewHeadersCount + if (mAdapter != null && mIndexer != null && mDoHeaderReset) { + + // Get the section header position + var sectionSize = 0 + val sectionPos = mIndexer!!.getHeaderPositionFromItemPosition(firstVisibleItem) + + // New section - set it in the header view + var newView = false + if (sectionPos != mCurrentSectionPos) { + + // No header for current position , use the nonessential invisible one, + // hide the separator + if (sectionPos == -1) { + sectionSize = 0 + this.removeView(mStickyHeader) + mStickyHeader = mNonessentialHeader + if (mSeparatorView != null) { + mSeparatorView?.setVisibility(View.GONE) + } + newView = true + } else { + // Create a copy of the header view to show on top + sectionSize = mIndexer!!.getHeaderItemsNumber(sectionPos) + val v: View? = + mAdapter?.getView(sectionPos + mListViewHeadersCount, null, mListView) + v?.measure( + MeasureSpec.makeMeasureSpec( + mListView?.getWidth() as Int, + MeasureSpec.EXACTLY + ), MeasureSpec.makeMeasureSpec( + mListView?.getHeight() as Int, + MeasureSpec.AT_MOST + ) + ) + this.removeView(mStickyHeader) + mStickyHeader = v + newView = true + } + mCurrentSectionPos = sectionPos + mNextSectionPosition = sectionSize + sectionPos + 1 + } + + // Do transitions + // If position of bottom of last item in a section is smaller than the height of the + // sticky header - shift drawable of header. + if (mStickyHeader != null) { + val sectionLastItemPosition = mNextSectionPosition - firstVisibleItem - 1 + var stickyHeaderHeight: Int = mStickyHeader?.getHeight() as Int + if (stickyHeaderHeight == 0) { + stickyHeaderHeight = mStickyHeader?.getMeasuredHeight() as Int + } + + // Update new header height + if (mHeaderHeightListener != null && + mLastStickyHeaderHeight != stickyHeaderHeight + ) { + mLastStickyHeaderHeight = stickyHeaderHeight + mHeaderHeightListener!!.OnHeaderHeightChanged(stickyHeaderHeight) + } + val SectionLastView: View? = mListView?.getChildAt(sectionLastItemPosition) + if (SectionLastView != null && SectionLastView.getBottom() <= stickyHeaderHeight) { + val lastViewBottom: Int = SectionLastView.getBottom() + mStickyHeader?.setTranslationY(lastViewBottom.toFloat() - + stickyHeaderHeight.toFloat()) + if (mSeparatorView != null) { + mSeparatorView?.setVisibility(View.GONE) + } + } else if (stickyHeaderHeight != 0) { + mStickyHeader?.setTranslationY(0f) + if (mSeparatorView != null && + mStickyHeader?.equals(mNonessentialHeader) == false) { + mSeparatorView?.setVisibility(View.VISIBLE) + } + } + if (newView) { + mStickyHeader?.setVisibility(View.INVISIBLE) + this.addView(mStickyHeader) + if (mSeparatorView != null && + mStickyHeader?.equals(mNonessentialHeader) == false) { + val params: FrameLayout.LayoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + mSeparatorWidth + ) + params.setMargins(0, mStickyHeader?.getMeasuredHeight() as Int, 0, 0) + mSeparatorView?.setLayoutParams(params) + mSeparatorView?.setVisibility(View.VISIBLE) + } + mStickyHeader?.setVisibility(View.VISIBLE) + } + } + } + } + + @Override + protected override fun onFinishInflate() { + super.onFinishInflate() + if (!mChildViewsCreated) { + setChildViews() + } + mDoHeaderReset = true + } + + @Override + protected override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (!mChildViewsCreated) { + setChildViews() + } + mDoHeaderReset = true + } + + // Resets the sticky header when the adapter data set was changed + // This code is needed only if dataset changes do not force a call to OnScroll + // protected void onDataChanged() { + // Should do a call to updateStickyHeader if needed + // } + private fun setChildViews() { + // Find a child ListView (if any) + val iChildNum: Int = getChildCount() + for (i in 0 until iChildNum) { + val v: Object = getChildAt(i) as Object + if (v is ListView) { + setListView(v as ListView) + } + } + + // No child ListView - add one + if (mListView == null) { + setListView(ListView(mContext)) + } + + // Create a nonessential view , it will be used in case a section has no header + mNonessentialHeader = View(mContext) + val params: ViewGroup.LayoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + 1, Gravity.TOP + ) + mNonessentialHeader?.setLayoutParams(params) + mNonessentialHeader?.setBackgroundColor(Color.TRANSPARENT) + mChildViewsCreated = true + } + + companion object { + private const val TAG = "StickyHeaderListView" + } + + /** + * Constructor + * + * @param context - application context. + * @param attrs - layout attributes. + */ + init { + mContext = context + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/UpgradeReceiver.java b/src/com/android/calendar/UpgradeReceiver.kt index 0e89286d..ab2de1de 100644 --- a/src/com/android/calendar/UpgradeReceiver.java +++ b/src/com/android/calendar/UpgradeReceiver.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * Copyright (C) 2021 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. @@ -13,17 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.calendar -package com.android.calendar; +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class UpgradeReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, final Intent intent) { - Utils.trySyncAndDisableUpgradeReceiver(context); +class UpgradeReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Utils.trySyncAndDisableUpgradeReceiver(context) } - }
\ No newline at end of file diff --git a/src/com/android/calendar/Utils.java b/src/com/android/calendar/Utils.java deleted file mode 100644 index cc55c999..00000000 --- a/src/com/android/calendar/Utils.java +++ /dev/null @@ -1,1499 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.calendar; - -import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; - -import android.accounts.Account; -import android.app.Activity; -import android.app.SearchManager; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.provider.CalendarContract.Calendars; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.text.style.URLSpan; -import android.text.util.Linkify; -import android.util.Log; - -import com.android.calendar.CalendarController.ViewType; -import com.android.calendar.CalendarUtils.TimeZoneUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Formatter; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Utils { - private static final boolean DEBUG = false; - private static final String TAG = "CalUtils"; - - // Set to 0 until we have UI to perform undo - public static final long UNDO_DELAY = 0; - - // For recurring events which instances of the series are being modified - public static final int MODIFY_UNINITIALIZED = 0; - public static final int MODIFY_SELECTED = 1; - public static final int MODIFY_ALL_FOLLOWING = 2; - public static final int MODIFY_ALL = 3; - - // When the edit event view finishes it passes back the appropriate exit - // code. - public static final int DONE_REVERT = 1 << 0; - public static final int DONE_SAVE = 1 << 1; - public static final int DONE_DELETE = 1 << 2; - // And should re run with DONE_EXIT if it should also leave the view, just - // exiting is identical to reverting - public static final int DONE_EXIT = 1 << 0; - - public static final String OPEN_EMAIL_MARKER = " <"; - public static final String CLOSE_EMAIL_MARKER = ">"; - - public static final String INTENT_KEY_DETAIL_VIEW = "DETAIL_VIEW"; - public static final String INTENT_KEY_VIEW_TYPE = "VIEW"; - public static final String INTENT_VALUE_VIEW_TYPE_DAY = "DAY"; - public static final String INTENT_KEY_HOME = "KEY_HOME"; - - public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3; - public static final int DECLINED_EVENT_ALPHA = 0x66; - public static final int DECLINED_EVENT_TEXT_ALPHA = 0xC0; - - private static final float SATURATION_ADJUST = 1.3f; - private static final float INTENSITY_ADJUST = 0.8f; - - // Defines used by the DNA generation code - static final int DAY_IN_MINUTES = 60 * 24; - static final int WEEK_IN_MINUTES = DAY_IN_MINUTES * 7; - // The work day is being counted as 6am to 8pm - static int WORK_DAY_MINUTES = 14 * 60; - static int WORK_DAY_START_MINUTES = 6 * 60; - static int WORK_DAY_END_MINUTES = 20 * 60; - static int WORK_DAY_END_LENGTH = (24 * 60) - WORK_DAY_END_MINUTES; - static int CONFLICT_COLOR = 0xFF000000; - static boolean mMinutesLoaded = false; - - public static final int YEAR_MIN = 1970; - public static final int YEAR_MAX = 2036; - - // The name of the shared preferences file. This name must be maintained for - // historical - // reasons, as it's what PreferenceManager assigned the first time the file - // was created. - static final String SHARED_PREFS_NAME = "com.android.calendar_preferences"; - - public static final String KEY_QUICK_RESPONSES = "preferences_quick_responses"; - - public static final String KEY_ALERTS_VIBRATE_WHEN = "preferences_alerts_vibrateWhen"; - - public static final String APPWIDGET_DATA_TYPE = "vnd.android.data/update"; - - static final String MACHINE_GENERATED_ADDRESS = "calendar.google.com"; - - private static final TimeZoneUtils mTZUtils = new TimeZoneUtils(SHARED_PREFS_NAME); - private static boolean mAllowWeekForDetailView = false; - private static long mTardis = 0; - private static String sVersion = null; - - private static final Pattern mWildcardPattern = Pattern.compile("^.*$"); - - /** - * A coordinate must be of the following form for Google Maps to correctly use it: - * Latitude, Longitude - * - * This may be in decimal form: - * Latitude: {-90 to 90} - * Longitude: {-180 to 180} - * - * Or, in degrees, minutes, and seconds: - * Latitude: {-90 to 90}° {0 to 59}' {0 to 59}" - * Latitude: {-180 to 180}° {0 to 59}' {0 to 59}" - * + or - degrees may also be represented with N or n, S or s for latitude, and with - * E or e, W or w for longitude, where the direction may either precede or follow the value. - * - * Some examples of coordinates that will be accepted by the regex: - * 37.422081°, -122.084576° - * 37.422081,-122.084576 - * +37°25'19.49", -122°5'4.47" - * 37°25'19.49"N, 122°5'4.47"W - * N 37° 25' 19.49", W 122° 5' 4.47" - **/ - private static final String COORD_DEGREES_LATITUDE = - "([-+NnSs]" + "(\\s)*)?" - + "[1-9]?[0-9](\u00B0)" + "(\\s)*" - + "([1-5]?[0-9]\')?" + "(\\s)*" - + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" - + "((\\s)*" + "[NnSs])?"; - private static final String COORD_DEGREES_LONGITUDE = - "([-+EeWw]" + "(\\s)*)?" - + "(1)?[0-9]?[0-9](\u00B0)" + "(\\s)*" - + "([1-5]?[0-9]\')?" + "(\\s)*" - + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" - + "((\\s)*" + "[EeWw])?"; - private static final String COORD_DEGREES_PATTERN = - COORD_DEGREES_LATITUDE - + "(\\s)*" + "," + "(\\s)*" - + COORD_DEGREES_LONGITUDE; - private static final String COORD_DECIMAL_LATITUDE = - "[+-]?" - + "[1-9]?[0-9]" + "(\\.[0-9]+)" - + "(\u00B0)?"; - private static final String COORD_DECIMAL_LONGITUDE = - "[+-]?" - + "(1)?[0-9]?[0-9]" + "(\\.[0-9]+)" - + "(\u00B0)?"; - private static final String COORD_DECIMAL_PATTERN = - COORD_DECIMAL_LATITUDE - + "(\\s)*" + "," + "(\\s)*" - + COORD_DECIMAL_LONGITUDE; - private static final Pattern COORD_PATTERN = - Pattern.compile(COORD_DEGREES_PATTERN + "|" + COORD_DECIMAL_PATTERN); - - private static final String NANP_ALLOWED_SYMBOLS = "()+-*#."; - private static final int NANP_MIN_DIGITS = 7; - private static final int NANP_MAX_DIGITS = 11; - - - /** - * Returns whether the SDK is the Jellybean release or later. - */ - public static boolean isJellybeanOrLater() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; - } - - /** - * Returns whether the SDK is the KeyLimePie release or later. - */ - public static boolean isKeyLimePieOrLater() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - } - - public static int getViewTypeFromIntentAndSharedPref(Activity activity) { - Intent intent = activity.getIntent(); - Bundle extras = intent.getExtras(); - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(activity); - - if (TextUtils.equals(intent.getAction(), Intent.ACTION_EDIT)) { - return ViewType.EDIT; - } - if (extras != null) { - if (extras.getBoolean(INTENT_KEY_DETAIL_VIEW, false)) { - // This is the "detail" view which is either agenda or day view - return prefs.getInt(GeneralPreferences.KEY_DETAILED_VIEW, - GeneralPreferences.DEFAULT_DETAILED_VIEW); - } else if (INTENT_VALUE_VIEW_TYPE_DAY.equals(extras.getString(INTENT_KEY_VIEW_TYPE))) { - // Not sure who uses this. This logic came from LaunchActivity - return ViewType.DAY; - } - } - - // Default to the last view - return prefs.getInt( - GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW); - } - - /** - * Gets the intent action for telling the widget to update. - */ - public static String getWidgetUpdateAction(Context context) { - return context.getPackageName() + ".APPWIDGET_UPDATE"; - } - - /** - * Gets the intent action for telling the widget to update. - */ - public static String getWidgetScheduledUpdateAction(Context context) { - return context.getPackageName() + ".APPWIDGET_SCHEDULED_UPDATE"; - } - - /** - * Writes a new home time zone to the db. Updates the home time zone in the - * db asynchronously and updates the local cache. Sending a time zone of - * **tbd** will cause it to be set to the device's time zone. null or empty - * tz will be ignored. - * - * @param context The calling activity - * @param timeZone The time zone to set Calendar to, or **tbd** - */ - public static void setTimeZone(Context context, String timeZone) { - mTZUtils.setTimeZone(context, timeZone); - } - - /** - * Gets the time zone that Calendar should be displayed in This is a helper - * method to get the appropriate time zone for Calendar. If this is the - * first time this method has been called it will initiate an asynchronous - * query to verify that the data in preferences is correct. The callback - * supplied will only be called if this query returns a value other than - * what is stored in preferences and should cause the calling activity to - * refresh anything that depends on calling this method. - * - * @param context The calling activity - * @param callback The runnable that should execute if a query returns new - * values - * @return The string value representing the time zone Calendar should - * display - */ - public static String getTimeZone(Context context, Runnable callback) { - return mTZUtils.getTimeZone(context, callback); - } - - /** - * Formats a date or a time range according to the local conventions. - * - * @param context the context is required only if the time is shown - * @param startMillis the start time in UTC milliseconds - * @param endMillis the end time in UTC milliseconds - * @param flags a bit mask of options See {@link DateUtils#formatDateRange(Context, Formatter, - * long, long, int, String) formatDateRange} - * @return a string containing the formatted date/time range. - */ - public static String formatDateRange( - Context context, long startMillis, long endMillis, int flags) { - return mTZUtils.formatDateRange(context, startMillis, endMillis, flags); - } - - public static boolean getDefaultVibrate(Context context, SharedPreferences prefs) { - boolean vibrate; - if (prefs.contains(KEY_ALERTS_VIBRATE_WHEN)) { - // Migrate setting to new 4.2 behavior - // - // silent and never -> off - // always -> on - String vibrateWhen = prefs.getString(KEY_ALERTS_VIBRATE_WHEN, null); - vibrate = vibrateWhen != null && vibrateWhen.equals(context - .getString(R.string.prefDefault_alerts_vibrate_true)); - prefs.edit().remove(KEY_ALERTS_VIBRATE_WHEN).commit(); - Log.d(TAG, "Migrating KEY_ALERTS_VIBRATE_WHEN(" + vibrateWhen - + ") to KEY_ALERTS_VIBRATE = " + vibrate); - } else { - vibrate = prefs.getBoolean(GeneralPreferences.KEY_ALERTS_VIBRATE, - false); - } - return vibrate; - } - - public static String[] getSharedPreference(Context context, String key, String[] defaultValue) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - Set<String> ss = prefs.getStringSet(key, null); - if (ss != null) { - String strings[] = new String[ss.size()]; - return ss.toArray(strings); - } - return defaultValue; - } - - public static String getSharedPreference(Context context, String key, String defaultValue) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - return prefs.getString(key, defaultValue); - } - - public static int getSharedPreference(Context context, String key, int defaultValue) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - return prefs.getInt(key, defaultValue); - } - - public static boolean getSharedPreference(Context context, String key, boolean defaultValue) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - return prefs.getBoolean(key, defaultValue); - } - - /** - * Asynchronously sets the preference with the given key to the given value - * - * @param context the context to use to get preferences from - * @param key the key of the preference to set - * @param value the value to set - */ - public static void setSharedPreference(Context context, String key, String value) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - prefs.edit().putString(key, value).apply(); - } - - public static void setSharedPreference(Context context, String key, String[] values) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - LinkedHashSet<String> set = new LinkedHashSet<String>(); - for (String value : values) { - set.add(value); - } - prefs.edit().putStringSet(key, set).apply(); - } - - protected static void tardis() { - mTardis = System.currentTimeMillis(); - } - - protected static long getTardis() { - return mTardis; - } - - public static void setSharedPreference(Context context, String key, boolean value) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(key, value); - editor.apply(); - } - - static void setSharedPreference(Context context, String key, int value) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(key, value); - editor.apply(); - } - - public static void removeSharedPreference(Context context, String key) { - SharedPreferences prefs = context.getSharedPreferences( - GeneralPreferences.SHARED_PREFS_NAME, Context.MODE_PRIVATE); - prefs.edit().remove(key).apply(); - } - - /** - * Save default agenda/day/week/month view for next time - * - * @param context - * @param viewId {@link CalendarController.ViewType} - */ - static void setDefaultView(Context context, int viewId) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - - boolean validDetailView = false; - if (mAllowWeekForDetailView && viewId == CalendarController.ViewType.WEEK) { - validDetailView = true; - } else { - validDetailView = viewId == CalendarController.ViewType.AGENDA - || viewId == CalendarController.ViewType.DAY; - } - - if (validDetailView) { - // Record the detail start view - editor.putInt(GeneralPreferences.KEY_DETAILED_VIEW, viewId); - } - - // Record the (new) start view - editor.putInt(GeneralPreferences.KEY_START_VIEW, viewId); - editor.apply(); - } - - public static MatrixCursor matrixCursorFromCursor(Cursor cursor) { - if (cursor == null) { - return null; - } - - String[] columnNames = cursor.getColumnNames(); - if (columnNames == null) { - columnNames = new String[] {}; - } - MatrixCursor newCursor = new MatrixCursor(columnNames); - int numColumns = cursor.getColumnCount(); - String data[] = new String[numColumns]; - cursor.moveToPosition(-1); - while (cursor.moveToNext()) { - for (int i = 0; i < numColumns; i++) { - data[i] = cursor.getString(i); - } - newCursor.addRow(data); - } - return newCursor; - } - - /** - * Compares two cursors to see if they contain the same data. - * - * @return Returns true of the cursors contain the same data and are not - * null, false otherwise - */ - public static boolean compareCursors(Cursor c1, Cursor c2) { - if (c1 == null || c2 == null) { - return false; - } - - int numColumns = c1.getColumnCount(); - if (numColumns != c2.getColumnCount()) { - return false; - } - - if (c1.getCount() != c2.getCount()) { - return false; - } - - c1.moveToPosition(-1); - c2.moveToPosition(-1); - while (c1.moveToNext() && c2.moveToNext()) { - for (int i = 0; i < numColumns; i++) { - if (!TextUtils.equals(c1.getString(i), c2.getString(i))) { - return false; - } - } - } - - return true; - } - - /** - * If the given intent specifies a time (in milliseconds since the epoch), - * then that time is returned. Otherwise, the current time is returned. - */ - public static final long timeFromIntentInMillis(Intent intent) { - // If the time was specified, then use that. Otherwise, use the current - // time. - Uri data = intent.getData(); - long millis = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, -1); - if (millis == -1 && data != null && data.isHierarchical()) { - List<String> path = data.getPathSegments(); - if (path.size() == 2 && path.get(0).equals("time")) { - try { - millis = Long.valueOf(data.getLastPathSegment()); - } catch (NumberFormatException e) { - Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time " - + "found. Using current time."); - } - } - } - if (millis <= 0) { - millis = System.currentTimeMillis(); - } - return millis; - } - - /** - * Formats the given Time object so that it gives the month and year (for - * example, "September 2007"). - * - * @param time the time to format - * @return the string containing the weekday and the date - */ - public static String formatMonthYear(Context context, Time time) { - int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY - | DateUtils.FORMAT_SHOW_YEAR; - long millis = time.toMillis(true); - return formatDateRange(context, millis, millis, flags); - } - - /** - * Returns a list joined together by the provided delimiter, for example, - * ["a", "b", "c"] could be joined into "a,b,c" - * - * @param things the things to join together - * @param delim the delimiter to use - * @return a string contained the things joined together - */ - public static String join(List<?> things, String delim) { - StringBuilder builder = new StringBuilder(); - boolean first = true; - for (Object thing : things) { - if (first) { - first = false; - } else { - builder.append(delim); - } - builder.append(thing.toString()); - } - return builder.toString(); - } - - /** - * Returns the week since {@link Time#EPOCH_JULIAN_DAY} (Jan 1, 1970) - * adjusted for first day of week. - * - * This takes a julian day and the week start day and calculates which - * week since {@link Time#EPOCH_JULIAN_DAY} that day occurs in, starting - * at 0. *Do not* use this to compute the ISO week number for the year. - * - * @param julianDay The julian day to calculate the week number for - * @param firstDayOfWeek Which week day is the first day of the week, - * see {@link Time#SUNDAY} - * @return Weeks since the epoch - */ - public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) { - int diff = Time.THURSDAY - firstDayOfWeek; - if (diff < 0) { - diff += 7; - } - int refDay = Time.EPOCH_JULIAN_DAY - diff; - return (julianDay - refDay) / 7; - } - - /** - * Takes a number of weeks since the epoch and calculates the Julian day of - * the Monday for that week. - * - * This assumes that the week containing the {@link Time#EPOCH_JULIAN_DAY} - * is considered week 0. It returns the Julian day for the Monday - * {@code week} weeks after the Monday of the week containing the epoch. - * - * @param week Number of weeks since the epoch - * @return The julian day for the Monday of the given week since the epoch - */ - public static int getJulianMondayFromWeeksSinceEpoch(int week) { - return MONDAY_BEFORE_JULIAN_EPOCH + week * 7; - } - - /** - * Get first day of week as android.text.format.Time constant. - * - * @return the first day of week in android.text.format.Time - */ - public static int getFirstDayOfWeek(Context context) { - SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - String pref = prefs.getString( - GeneralPreferences.KEY_WEEK_START_DAY, GeneralPreferences.WEEK_START_DEFAULT); - - int startDay; - if (GeneralPreferences.WEEK_START_DEFAULT.equals(pref)) { - startDay = Calendar.getInstance().getFirstDayOfWeek(); - } else { - startDay = Integer.parseInt(pref); - } - - if (startDay == Calendar.SATURDAY) { - return Time.SATURDAY; - } else if (startDay == Calendar.MONDAY) { - return Time.MONDAY; - } else { - return Time.SUNDAY; - } - } - - /** - * Get first day of week as java.util.Calendar constant. - * - * @return the first day of week as a java.util.Calendar constant - */ - public static int getFirstDayOfWeekAsCalendar(Context context) { - return convertDayOfWeekFromTimeToCalendar(getFirstDayOfWeek(context)); - } - - /** - * Converts the day of the week from android.text.format.Time to java.util.Calendar - */ - public static int convertDayOfWeekFromTimeToCalendar(int timeDayOfWeek) { - switch (timeDayOfWeek) { - case Time.MONDAY: - return Calendar.MONDAY; - case Time.TUESDAY: - return Calendar.TUESDAY; - case Time.WEDNESDAY: - return Calendar.WEDNESDAY; - case Time.THURSDAY: - return Calendar.THURSDAY; - case Time.FRIDAY: - return Calendar.FRIDAY; - case Time.SATURDAY: - return Calendar.SATURDAY; - case Time.SUNDAY: - return Calendar.SUNDAY; - default: - throw new IllegalArgumentException("Argument must be between Time.SUNDAY and " + - "Time.SATURDAY"); - } - } - - /** - * @return true when week number should be shown. - */ - public static boolean getShowWeekNumber(Context context) { - final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - return prefs.getBoolean( - GeneralPreferences.KEY_SHOW_WEEK_NUM, GeneralPreferences.DEFAULT_SHOW_WEEK_NUM); - } - - /** - * @return true when declined events should be hidden. - */ - public static boolean getHideDeclinedEvents(Context context) { - final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - return prefs.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, false); - } - - public static int getDaysPerWeek(Context context) { - final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); - return prefs.getInt(GeneralPreferences.KEY_DAYS_PER_WEEK, 7); - } - - /** - * Determine whether the column position is Saturday or not. - * - * @param column the column position - * @param firstDayOfWeek the first day of week in android.text.format.Time - * @return true if the column is Saturday position - */ - public static boolean isSaturday(int column, int firstDayOfWeek) { - return (firstDayOfWeek == Time.SUNDAY && column == 6) - || (firstDayOfWeek == Time.MONDAY && column == 5) - || (firstDayOfWeek == Time.SATURDAY && column == 0); - } - - /** - * Determine whether the column position is Sunday or not. - * - * @param column the column position - * @param firstDayOfWeek the first day of week in android.text.format.Time - * @return true if the column is Sunday position - */ - public static boolean isSunday(int column, int firstDayOfWeek) { - return (firstDayOfWeek == Time.SUNDAY && column == 0) - || (firstDayOfWeek == Time.MONDAY && column == 6) - || (firstDayOfWeek == Time.SATURDAY && column == 1); - } - - /** - * Convert given UTC time into current local time. This assumes it is for an - * allday event and will adjust the time to be on a midnight boundary. - * - * @param recycle Time object to recycle, otherwise null. - * @param utcTime Time to convert, in UTC. - * @param tz The time zone to convert this time to. - */ - public static long convertAlldayUtcToLocal(Time recycle, long utcTime, String tz) { - if (recycle == null) { - recycle = new Time(); - } - recycle.timezone = Time.TIMEZONE_UTC; - recycle.set(utcTime); - recycle.timezone = tz; - return recycle.normalize(true); - } - - public static long convertAlldayLocalToUTC(Time recycle, long localTime, String tz) { - if (recycle == null) { - recycle = new Time(); - } - recycle.timezone = tz; - recycle.set(localTime); - recycle.timezone = Time.TIMEZONE_UTC; - return recycle.normalize(true); - } - - /** - * Finds and returns the next midnight after "theTime" in milliseconds UTC - * - * @param recycle - Time object to recycle, otherwise null. - * @param theTime - Time used for calculations (in UTC) - * @param tz The time zone to convert this time to. - */ - public static long getNextMidnight(Time recycle, long theTime, String tz) { - if (recycle == null) { - recycle = new Time(); - } - recycle.timezone = tz; - recycle.set(theTime); - recycle.monthDay ++; - recycle.hour = 0; - recycle.minute = 0; - recycle.second = 0; - return recycle.normalize(true); - } - - public static void setAllowWeekForDetailView(boolean allowWeekView) { - mAllowWeekForDetailView = allowWeekView; - } - - public static boolean getAllowWeekForDetailView() { - return mAllowWeekForDetailView; - } - - public static boolean getConfigBool(Context c, int key) { - return c.getResources().getBoolean(key); - } - - /** - * For devices with Jellybean or later, darkens the given color to ensure that white text is - * clearly visible on top of it. For devices prior to Jellybean, does nothing, as the - * sync adapter handles the color change. - * - * @param color - */ - public static int getDisplayColorFromColor(int color) { - if (!isJellybeanOrLater()) { - return color; - } - - float[] hsv = new float[3]; - Color.colorToHSV(color, hsv); - hsv[1] = Math.min(hsv[1] * SATURATION_ADJUST, 1.0f); - hsv[2] = hsv[2] * INTENSITY_ADJUST; - return Color.HSVToColor(hsv); - } - - // This takes a color and computes what it would look like blended with - // white. The result is the color that should be used for declined events. - public static int getDeclinedColorFromColor(int color) { - int bg = 0xffffffff; - int a = DECLINED_EVENT_ALPHA; - int r = (((color & 0x00ff0000) * a) + ((bg & 0x00ff0000) * (0xff - a))) & 0xff000000; - int g = (((color & 0x0000ff00) * a) + ((bg & 0x0000ff00) * (0xff - a))) & 0x00ff0000; - int b = (((color & 0x000000ff) * a) + ((bg & 0x000000ff) * (0xff - a))) & 0x0000ff00; - return (0xff000000) | ((r | g | b) >> 8); - } - - public static void trySyncAndDisableUpgradeReceiver(Context context) { - final PackageManager pm = context.getPackageManager(); - ComponentName upgradeComponent = new ComponentName(context, UpgradeReceiver.class); - if (pm.getComponentEnabledSetting(upgradeComponent) == - PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { - // The upgrade receiver has been disabled, which means this code has been run before, - // so no need to sync. - return; - } - - Bundle extras = new Bundle(); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - ContentResolver.requestSync( - null /* no account */, - Calendars.CONTENT_URI.getAuthority(), - extras); - - // Now unregister the receiver so that we won't continue to sync every time. - pm.setComponentEnabledSetting(upgradeComponent, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); - } - - // A single strand represents one color of events. Events are divided up by - // color to make them convenient to draw. The black strand is special in - // that it holds conflicting events as well as color settings for allday on - // each day. - public static class DNAStrand { - public float[] points; - public int[] allDays; // color for the allday, 0 means no event - int position; - public int color; - int count; - } - - // A segment is a single continuous length of time occupied by a single - // color. Segments should never span multiple days. - private static class DNASegment { - int startMinute; // in minutes since the start of the week - int endMinute; - int color; // Calendar color or black for conflicts - int day; // quick reference to the day this segment is on - } - - /** - * Converts a list of events to a list of segments to draw. Assumes list is - * ordered by start time of the events. The function processes events for a - * range of days from firstJulianDay to firstJulianDay + dayXs.length - 1. - * The algorithm goes over all the events and creates a set of segments - * ordered by start time. This list of segments is then converted into a - * HashMap of strands which contain the draw points and are organized by - * color. The strands can then be drawn by setting the paint color to each - * strand's color and calling drawLines on its set of points. The points are - * set up using the following parameters. - * <ul> - * <li>Events between midnight and WORK_DAY_START_MINUTES are compressed - * into the first 1/8th of the space between top and bottom.</li> - * <li>Events between WORK_DAY_END_MINUTES and the following midnight are - * compressed into the last 1/8th of the space between top and bottom</li> - * <li>Events between WORK_DAY_START_MINUTES and WORK_DAY_END_MINUTES use - * the remaining 3/4ths of the space</li> - * <li>All segments drawn will maintain at least minPixels height, except - * for conflicts in the first or last 1/8th, which may be smaller</li> - * </ul> - * - * @param firstJulianDay The julian day of the first day of events - * @param events A list of events sorted by start time - * @param top The lowest y value the dna should be drawn at - * @param bottom The highest y value the dna should be drawn at - * @param dayXs An array of x values to draw the dna at, one for each day - * @param conflictColor the color to use for conflicts - * @return - */ - public static HashMap<Integer, DNAStrand> createDNAStrands(int firstJulianDay, - ArrayList<Event> events, int top, int bottom, int minPixels, int[] dayXs, - Context context) { - - if (!mMinutesLoaded) { - if (context == null) { - Log.wtf(TAG, "No context and haven't loaded parameters yet! Can't create DNA."); - } - Resources res = context.getResources(); - CONFLICT_COLOR = res.getColor(R.color.month_dna_conflict_time_color); - WORK_DAY_START_MINUTES = res.getInteger(R.integer.work_start_minutes); - WORK_DAY_END_MINUTES = res.getInteger(R.integer.work_end_minutes); - WORK_DAY_END_LENGTH = DAY_IN_MINUTES - WORK_DAY_END_MINUTES; - WORK_DAY_MINUTES = WORK_DAY_END_MINUTES - WORK_DAY_START_MINUTES; - mMinutesLoaded = true; - } - - if (events == null || events.isEmpty() || dayXs == null || dayXs.length < 1 - || bottom - top < 8 || minPixels < 0) { - Log.e(TAG, - "Bad values for createDNAStrands! events:" + events + " dayXs:" - + Arrays.toString(dayXs) + " bot-top:" + (bottom - top) + " minPixels:" - + minPixels); - return null; - } - - LinkedList<DNASegment> segments = new LinkedList<DNASegment>(); - HashMap<Integer, DNAStrand> strands = new HashMap<Integer, DNAStrand>(); - // add a black strand by default, other colors will get added in - // the loop - DNAStrand blackStrand = new DNAStrand(); - blackStrand.color = CONFLICT_COLOR; - strands.put(CONFLICT_COLOR, blackStrand); - // the min length is the number of minutes that will occupy - // MIN_SEGMENT_PIXELS in the 'work day' time slot. This computes the - // minutes/pixel * minpx where the number of pixels are 3/4 the total - // dna height: 4*(mins/(px * 3/4)) - int minMinutes = minPixels * 4 * WORK_DAY_MINUTES / (3 * (bottom - top)); - - // There are slightly fewer than half as many pixels in 1/6 the space, - // so round to 2.5x for the min minutes in the non-work area - int minOtherMinutes = minMinutes * 5 / 2; - int lastJulianDay = firstJulianDay + dayXs.length - 1; - - Event event = new Event(); - // Go through all the events for the week - for (Event currEvent : events) { - // if this event is outside the weeks range skip it - if (currEvent.endDay < firstJulianDay || currEvent.startDay > lastJulianDay) { - continue; - } - if (currEvent.drawAsAllday()) { - addAllDayToStrands(currEvent, strands, firstJulianDay, dayXs.length); - continue; - } - // Copy the event over so we can clip its start and end to our range - currEvent.copyTo(event); - if (event.startDay < firstJulianDay) { - event.startDay = firstJulianDay; - event.startTime = 0; - } - // If it starts after the work day make sure the start is at least - // minPixels from midnight - if (event.startTime > DAY_IN_MINUTES - minOtherMinutes) { - event.startTime = DAY_IN_MINUTES - minOtherMinutes; - } - if (event.endDay > lastJulianDay) { - event.endDay = lastJulianDay; - event.endTime = DAY_IN_MINUTES - 1; - } - // If the end time is before the work day make sure it ends at least - // minPixels after midnight - if (event.endTime < minOtherMinutes) { - event.endTime = minOtherMinutes; - } - // If the start and end are on the same day make sure they are at - // least minPixels apart. This only needs to be done for times - // outside the work day as the min distance for within the work day - // is enforced in the segment code. - if (event.startDay == event.endDay && - event.endTime - event.startTime < minOtherMinutes) { - // If it's less than minPixels in an area before the work - // day - if (event.startTime < WORK_DAY_START_MINUTES) { - // extend the end to the first easy guarantee that it's - // minPixels - event.endTime = Math.min(event.startTime + minOtherMinutes, - WORK_DAY_START_MINUTES + minMinutes); - // if it's in the area after the work day - } else if (event.endTime > WORK_DAY_END_MINUTES) { - // First try shifting the end but not past midnight - event.endTime = Math.min(event.endTime + minOtherMinutes, DAY_IN_MINUTES - 1); - // if it's still too small move the start back - if (event.endTime - event.startTime < minOtherMinutes) { - event.startTime = event.endTime - minOtherMinutes; - } - } - } - - // This handles adding the first segment - if (segments.size() == 0) { - addNewSegment(segments, event, strands, firstJulianDay, 0, minMinutes); - continue; - } - // Now compare our current start time to the end time of the last - // segment in the list - DNASegment lastSegment = segments.getLast(); - int startMinute = (event.startDay - firstJulianDay) * DAY_IN_MINUTES + event.startTime; - int endMinute = Math.max((event.endDay - firstJulianDay) * DAY_IN_MINUTES - + event.endTime, startMinute + minMinutes); - - if (startMinute < 0) { - startMinute = 0; - } - if (endMinute >= WEEK_IN_MINUTES) { - endMinute = WEEK_IN_MINUTES - 1; - } - // If we start before the last segment in the list ends we need to - // start going through the list as this may conflict with other - // events - if (startMinute < lastSegment.endMinute) { - int i = segments.size(); - // find the last segment this event intersects with - while (--i >= 0 && endMinute < segments.get(i).startMinute); - - DNASegment currSegment; - // for each segment this event intersects with - for (; i >= 0 && startMinute <= (currSegment = segments.get(i)).endMinute; i--) { - // if the segment is already a conflict ignore it - if (currSegment.color == CONFLICT_COLOR) { - continue; - } - // if the event ends before the segment and wouldn't create - // a segment that is too small split off the right side - if (endMinute < currSegment.endMinute - minMinutes) { - DNASegment rhs = new DNASegment(); - rhs.endMinute = currSegment.endMinute; - rhs.color = currSegment.color; - rhs.startMinute = endMinute + 1; - rhs.day = currSegment.day; - currSegment.endMinute = endMinute; - segments.add(i + 1, rhs); - strands.get(rhs.color).count++; - if (DEBUG) { - Log.d(TAG, "Added rhs, curr:" + currSegment.toString() + " i:" - + segments.get(i).toString()); - } - } - // if the event starts after the segment and wouldn't create - // a segment that is too small split off the left side - if (startMinute > currSegment.startMinute + minMinutes) { - DNASegment lhs = new DNASegment(); - lhs.startMinute = currSegment.startMinute; - lhs.color = currSegment.color; - lhs.endMinute = startMinute - 1; - lhs.day = currSegment.day; - currSegment.startMinute = startMinute; - // increment i so that we are at the right position when - // referencing the segments to the right and left of the - // current segment. - segments.add(i++, lhs); - strands.get(lhs.color).count++; - if (DEBUG) { - Log.d(TAG, "Added lhs, curr:" + currSegment.toString() + " i:" - + segments.get(i).toString()); - } - } - // if the right side is black merge this with the segment to - // the right if they're on the same day and overlap - if (i + 1 < segments.size()) { - DNASegment rhs = segments.get(i + 1); - if (rhs.color == CONFLICT_COLOR && currSegment.day == rhs.day - && rhs.startMinute <= currSegment.endMinute + 1) { - rhs.startMinute = Math.min(currSegment.startMinute, rhs.startMinute); - segments.remove(currSegment); - strands.get(currSegment.color).count--; - // point at the new current segment - currSegment = rhs; - } - } - // if the left side is black merge this with the segment to - // the left if they're on the same day and overlap - if (i - 1 >= 0) { - DNASegment lhs = segments.get(i - 1); - if (lhs.color == CONFLICT_COLOR && currSegment.day == lhs.day - && lhs.endMinute >= currSegment.startMinute - 1) { - lhs.endMinute = Math.max(currSegment.endMinute, lhs.endMinute); - segments.remove(currSegment); - strands.get(currSegment.color).count--; - // point at the new current segment - currSegment = lhs; - // point i at the new current segment in case new - // code is added - i--; - } - } - // if we're still not black, decrement the count for the - // color being removed, change this to black, and increment - // the black count - if (currSegment.color != CONFLICT_COLOR) { - strands.get(currSegment.color).count--; - currSegment.color = CONFLICT_COLOR; - strands.get(CONFLICT_COLOR).count++; - } - } - - } - // If this event extends beyond the last segment add a new segment - if (endMinute > lastSegment.endMinute) { - addNewSegment(segments, event, strands, firstJulianDay, lastSegment.endMinute, - minMinutes); - } - } - weaveDNAStrands(segments, firstJulianDay, strands, top, bottom, dayXs); - return strands; - } - - // This figures out allDay colors as allDay events are found - private static void addAllDayToStrands(Event event, HashMap<Integer, DNAStrand> strands, - int firstJulianDay, int numDays) { - DNAStrand strand = getOrCreateStrand(strands, CONFLICT_COLOR); - // if we haven't initialized the allDay portion create it now - if (strand.allDays == null) { - strand.allDays = new int[numDays]; - } - - // For each day this event is on update the color - int end = Math.min(event.endDay - firstJulianDay, numDays - 1); - for (int i = Math.max(event.startDay - firstJulianDay, 0); i <= end; i++) { - if (strand.allDays[i] != 0) { - // if this day already had a color, it is now a conflict - strand.allDays[i] = CONFLICT_COLOR; - } else { - // else it's just the color of the event - strand.allDays[i] = event.color; - } - } - } - - // This processes all the segments, sorts them by color, and generates a - // list of points to draw - private static void weaveDNAStrands(LinkedList<DNASegment> segments, int firstJulianDay, - HashMap<Integer, DNAStrand> strands, int top, int bottom, int[] dayXs) { - // First, get rid of any colors that ended up with no segments - Iterator<DNAStrand> strandIterator = strands.values().iterator(); - while (strandIterator.hasNext()) { - DNAStrand strand = strandIterator.next(); - if (strand.count < 1 && strand.allDays == null) { - strandIterator.remove(); - continue; - } - strand.points = new float[strand.count * 4]; - strand.position = 0; - } - // Go through each segment and compute its points - for (DNASegment segment : segments) { - // Add the points to the strand of that color - DNAStrand strand = strands.get(segment.color); - int dayIndex = segment.day - firstJulianDay; - int dayStartMinute = segment.startMinute % DAY_IN_MINUTES; - int dayEndMinute = segment.endMinute % DAY_IN_MINUTES; - int height = bottom - top; - int workDayHeight = height * 3 / 4; - int remainderHeight = (height - workDayHeight) / 2; - - int x = dayXs[dayIndex]; - int y0 = 0; - int y1 = 0; - - y0 = top + getPixelOffsetFromMinutes(dayStartMinute, workDayHeight, remainderHeight); - y1 = top + getPixelOffsetFromMinutes(dayEndMinute, workDayHeight, remainderHeight); - if (DEBUG) { - Log.d(TAG, "Adding " + Integer.toHexString(segment.color) + " at x,y0,y1: " + x - + " " + y0 + " " + y1 + " for " + dayStartMinute + " " + dayEndMinute); - } - strand.points[strand.position++] = x; - strand.points[strand.position++] = y0; - strand.points[strand.position++] = x; - strand.points[strand.position++] = y1; - } - } - - /** - * Compute a pixel offset from the top for a given minute from the work day - * height and the height of the top area. - */ - private static int getPixelOffsetFromMinutes(int minute, int workDayHeight, - int remainderHeight) { - int y; - if (minute < WORK_DAY_START_MINUTES) { - y = minute * remainderHeight / WORK_DAY_START_MINUTES; - } else if (minute < WORK_DAY_END_MINUTES) { - y = remainderHeight + (minute - WORK_DAY_START_MINUTES) * workDayHeight - / WORK_DAY_MINUTES; - } else { - y = remainderHeight + workDayHeight + (minute - WORK_DAY_END_MINUTES) * remainderHeight - / WORK_DAY_END_LENGTH; - } - return y; - } - - /** - * Add a new segment based on the event provided. This will handle splitting - * segments across day boundaries and ensures a minimum size for segments. - */ - private static void addNewSegment(LinkedList<DNASegment> segments, Event event, - HashMap<Integer, DNAStrand> strands, int firstJulianDay, int minStart, int minMinutes) { - if (event.startDay > event.endDay) { - Log.wtf(TAG, "Event starts after it ends: " + event.toString()); - } - // If this is a multiday event split it up by day - if (event.startDay != event.endDay) { - Event lhs = new Event(); - lhs.color = event.color; - lhs.startDay = event.startDay; - // the first day we want the start time to be the actual start time - lhs.startTime = event.startTime; - lhs.endDay = lhs.startDay; - lhs.endTime = DAY_IN_MINUTES - 1; - // Nearly recursive iteration! - while (lhs.startDay != event.endDay) { - addNewSegment(segments, lhs, strands, firstJulianDay, minStart, minMinutes); - // The days in between are all day, even though that shouldn't - // actually happen due to the allday filtering - lhs.startDay++; - lhs.endDay = lhs.startDay; - lhs.startTime = 0; - minStart = 0; - } - // The last day we want the end time to be the actual end time - lhs.endTime = event.endTime; - event = lhs; - } - // Create the new segment and compute its fields - DNASegment segment = new DNASegment(); - int dayOffset = (event.startDay - firstJulianDay) * DAY_IN_MINUTES; - int endOfDay = dayOffset + DAY_IN_MINUTES - 1; - // clip the start if needed - segment.startMinute = Math.max(dayOffset + event.startTime, minStart); - // and extend the end if it's too small, but not beyond the end of the - // day - int minEnd = Math.min(segment.startMinute + minMinutes, endOfDay); - segment.endMinute = Math.max(dayOffset + event.endTime, minEnd); - if (segment.endMinute > endOfDay) { - segment.endMinute = endOfDay; - } - - segment.color = event.color; - segment.day = event.startDay; - segments.add(segment); - // increment the count for the correct color or add a new strand if we - // don't have that color yet - DNAStrand strand = getOrCreateStrand(strands, segment.color); - strand.count++; - } - - /** - * Try to get a strand of the given color. Create it if it doesn't exist. - */ - private static DNAStrand getOrCreateStrand(HashMap<Integer, DNAStrand> strands, int color) { - DNAStrand strand = strands.get(color); - if (strand == null) { - strand = new DNAStrand(); - strand.color = color; - strand.count = 0; - strands.put(strand.color, strand); - } - return strand; - } - - /** - * Sends an intent to launch the top level Calendar view. - * - * @param context - */ - public static void returnToCalendarHome(Context context) { - Intent launchIntent = new Intent(context, AllInOneActivity.class); - launchIntent.setAction(Intent.ACTION_DEFAULT); - launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - launchIntent.putExtra(INTENT_KEY_HOME, true); - context.startActivity(launchIntent); - } - - /** - * Given a context and a time in millis since unix epoch figures out the - * correct week of the year for that time. - * - * @param millisSinceEpoch - * @return - */ - public static int getWeekNumberFromTime(long millisSinceEpoch, Context context) { - Time weekTime = new Time(getTimeZone(context, null)); - weekTime.set(millisSinceEpoch); - weekTime.normalize(true); - int firstDayOfWeek = getFirstDayOfWeek(context); - // if the date is on Saturday or Sunday and the start of the week - // isn't Monday we may need to shift the date to be in the correct - // week - if (weekTime.weekDay == Time.SUNDAY - && (firstDayOfWeek == Time.SUNDAY || firstDayOfWeek == Time.SATURDAY)) { - weekTime.monthDay++; - weekTime.normalize(true); - } else if (weekTime.weekDay == Time.SATURDAY && firstDayOfWeek == Time.SATURDAY) { - weekTime.monthDay += 2; - weekTime.normalize(true); - } - return weekTime.getWeekNumber(); - } - - /** - * Formats a day of the week string. This is either just the name of the day - * or a combination of yesterday/today/tomorrow and the day of the week. - * - * @param julianDay The julian day to get the string for - * @param todayJulianDay The julian day for today's date - * @param millis A utc millis since epoch time that falls on julian day - * @param context The calling context, used to get the timezone and do the - * formatting - * @return - */ - public static String getDayOfWeekString(int julianDay, int todayJulianDay, long millis, - Context context) { - getTimeZone(context, null); - int flags = DateUtils.FORMAT_SHOW_WEEKDAY; - String dayViewText; - if (julianDay == todayJulianDay) { - dayViewText = context.getString(R.string.agenda_today, - mTZUtils.formatDateRange(context, millis, millis, flags).toString()); - } else if (julianDay == todayJulianDay - 1) { - dayViewText = context.getString(R.string.agenda_yesterday, - mTZUtils.formatDateRange(context, millis, millis, flags).toString()); - } else if (julianDay == todayJulianDay + 1) { - dayViewText = context.getString(R.string.agenda_tomorrow, - mTZUtils.formatDateRange(context, millis, millis, flags).toString()); - } else { - dayViewText = mTZUtils.formatDateRange(context, millis, millis, flags).toString(); - } - dayViewText = dayViewText.toUpperCase(); - return dayViewText; - } - - // Calculate the time until midnight + 1 second and set the handler to - // do run the runnable - public static void setMidnightUpdater(Handler h, Runnable r, String timezone) { - if (h == null || r == null || timezone == null) { - return; - } - long now = System.currentTimeMillis(); - Time time = new Time(timezone); - time.set(now); - long runInMillis = (24 * 3600 - time.hour * 3600 - time.minute * 60 - - time.second + 1) * 1000; - h.removeCallbacks(r); - h.postDelayed(r, runInMillis); - } - - // Stop the midnight update thread - public static void resetMidnightUpdater(Handler h, Runnable r) { - if (h == null || r == null) { - return; - } - h.removeCallbacks(r); - } - - /** - * Returns a string description of the specified time interval. - */ - public static String getDisplayedDatetime(long startMillis, long endMillis, long currentMillis, - String localTimezone, boolean allDay, Context context) { - // Configure date/time formatting. - int flagsDate = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY; - int flagsTime = DateUtils.FORMAT_SHOW_TIME; - if (DateFormat.is24HourFormat(context)) { - flagsTime |= DateUtils.FORMAT_24HOUR; - } - - Time currentTime = new Time(localTimezone); - currentTime.set(currentMillis); - Resources resources = context.getResources(); - String datetimeString = null; - if (allDay) { - // All day events require special timezone adjustment. - long localStartMillis = convertAlldayUtcToLocal(null, startMillis, localTimezone); - long localEndMillis = convertAlldayUtcToLocal(null, endMillis, localTimezone); - if (singleDayEvent(localStartMillis, localEndMillis, currentTime.gmtoff)) { - // If possible, use "Today" or "Tomorrow" instead of a full date string. - int todayOrTomorrow = isTodayOrTomorrow(context.getResources(), - localStartMillis, currentMillis, currentTime.gmtoff); - if (TODAY == todayOrTomorrow) { - datetimeString = resources.getString(R.string.today); - } else if (TOMORROW == todayOrTomorrow) { - datetimeString = resources.getString(R.string.tomorrow); - } - } - if (datetimeString == null) { - // For multi-day allday events or single-day all-day events that are not - // today or tomorrow, use framework formatter. - Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault()); - datetimeString = DateUtils.formatDateRange(context, f, startMillis, - endMillis, flagsDate, Time.TIMEZONE_UTC).toString(); - } - } else { - if (singleDayEvent(startMillis, endMillis, currentTime.gmtoff)) { - // Format the time. - String timeString = Utils.formatDateRange(context, startMillis, endMillis, - flagsTime); - - // If possible, use "Today" or "Tomorrow" instead of a full date string. - int todayOrTomorrow = isTodayOrTomorrow(context.getResources(), startMillis, - currentMillis, currentTime.gmtoff); - if (TODAY == todayOrTomorrow) { - // Example: "Today at 1:00pm - 2:00 pm" - datetimeString = resources.getString(R.string.today_at_time_fmt, - timeString); - } else if (TOMORROW == todayOrTomorrow) { - // Example: "Tomorrow at 1:00pm - 2:00 pm" - datetimeString = resources.getString(R.string.tomorrow_at_time_fmt, - timeString); - } else { - // Format the full date. Example: "Thursday, April 12, 1:00pm - 2:00pm" - String dateString = Utils.formatDateRange(context, startMillis, endMillis, - flagsDate); - datetimeString = resources.getString(R.string.date_time_fmt, dateString, - timeString); - } - } else { - // For multiday events, shorten day/month names. - // Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm" - int flagsDatetime = flagsDate | flagsTime | DateUtils.FORMAT_ABBREV_MONTH | - DateUtils.FORMAT_ABBREV_WEEKDAY; - datetimeString = Utils.formatDateRange(context, startMillis, endMillis, - flagsDatetime); - } - } - return datetimeString; - } - - /** - * Returns the timezone to display in the event info, if the local timezone is different - * from the event timezone. Otherwise returns null. - */ - public static String getDisplayedTimezone(long startMillis, String localTimezone, - String eventTimezone) { - String tzDisplay = null; - if (!TextUtils.equals(localTimezone, eventTimezone)) { - // Figure out if this is in DST - TimeZone tz = TimeZone.getTimeZone(localTimezone); - if (tz == null || tz.getID().equals("GMT")) { - tzDisplay = localTimezone; - } else { - Time startTime = new Time(localTimezone); - startTime.set(startMillis); - tzDisplay = tz.getDisplayName(startTime.isDst != 0, TimeZone.SHORT); - } - } - return tzDisplay; - } - - /** - * Returns whether the specified time interval is in a single day. - */ - private static boolean singleDayEvent(long startMillis, long endMillis, long localGmtOffset) { - if (startMillis == endMillis) { - return true; - } - - // An event ending at midnight should still be a single-day event, so check - // time end-1. - int startDay = Time.getJulianDay(startMillis, localGmtOffset); - int endDay = Time.getJulianDay(endMillis - 1, localGmtOffset); - return startDay == endDay; - } - - // Using int constants as a return value instead of an enum to minimize resources. - private static final int TODAY = 1; - private static final int TOMORROW = 2; - private static final int NONE = 0; - - /** - * Returns TODAY or TOMORROW if applicable. Otherwise returns NONE. - */ - private static int isTodayOrTomorrow(Resources r, long dayMillis, - long currentMillis, long localGmtOffset) { - int startDay = Time.getJulianDay(dayMillis, localGmtOffset); - int currentDay = Time.getJulianDay(currentMillis, localGmtOffset); - - int days = startDay - currentDay; - if (days == 1) { - return TOMORROW; - } else if (days == 0) { - return TODAY; - } else { - return NONE; - } - } - - /** - * Inserts a drawable with today's day into the today's icon in the option menu - * @param icon - today's icon from the options menu - */ - public static void setTodayIcon(LayerDrawable icon, Context c, String timezone) { - DayOfMonthDrawable today; - - // Reuse current drawable if possible - Drawable currentDrawable = icon.findDrawableByLayerId(R.id.today_icon_day); - if (currentDrawable != null && currentDrawable instanceof DayOfMonthDrawable) { - today = (DayOfMonthDrawable)currentDrawable; - } else { - today = new DayOfMonthDrawable(c); - } - // Set the day and update the icon - Time now = new Time(timezone); - now.setToNow(); - now.normalize(false); - today.setDayOfMonth(now.monthDay); - icon.mutate(); - icon.setDrawableByLayerId(R.id.today_icon_day, today); - } - - /** - * Get a list of quick responses used for emailing guests from the - * SharedPreferences. If not are found, get the hard coded ones that shipped - * with the app - * - * @param context - * @return a list of quick responses. - */ - public static String[] getQuickResponses(Context context) { - String[] s = Utils.getSharedPreference(context, KEY_QUICK_RESPONSES, (String[]) null); - - if (s == null) { - s = context.getResources().getStringArray(R.array.quick_response_defaults); - } - - return s; - } - - /** - * Return the app version code. - */ - public static String getVersionCode(Context context) { - if (sVersion == null) { - try { - sVersion = context.getPackageManager().getPackageInfo( - context.getPackageName(), 0).versionName; - } catch (PackageManager.NameNotFoundException e) { - // Can't find version; just leave it blank. - Log.e(TAG, "Error finding package " + context.getApplicationInfo().packageName); - } - } - return sVersion; - } -} diff --git a/src/com/android/calendar/Utils.kt b/src/com/android/calendar/Utils.kt new file mode 100644 index 00000000..ef780485 --- /dev/null +++ b/src/com/android/calendar/Utils.kt @@ -0,0 +1,1577 @@ +/* + * Copyright (C) 2021 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.calendar + +import android.app.Activity +import android.content.ComponentName +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.content.res.Resources +import android.database.Cursor +import android.database.MatrixCursor +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME +import android.text.TextUtils +import android.text.format.DateFormat +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log +import com.android.calendar.CalendarController.ViewType +import com.android.calendar.CalendarUtils.TimeZoneUtils +import java.util.ArrayList +import java.util.Arrays +import java.util.Calendar +import java.util.Formatter +import java.util.HashMap +import java.util.LinkedHashSet +import java.util.LinkedList +import java.util.List +import java.util.Locale +import java.util.TimeZone +import java.util.regex.Pattern + +object Utils { + private const val DEBUG = false + private const val TAG = "CalUtils" + + // Set to 0 until we have UI to perform undo + const val UNDO_DELAY: Long = 0 + + // For recurring events which instances of the series are being modified + const val MODIFY_UNINITIALIZED = 0 + const val MODIFY_SELECTED = 1 + const val MODIFY_ALL_FOLLOWING = 2 + const val MODIFY_ALL = 3 + + // When the edit event view finishes it passes back the appropriate exit + // code. + const val DONE_REVERT = 1 shl 0 + const val DONE_SAVE = 1 shl 1 + const val DONE_DELETE = 1 shl 2 + + // And should re run with DONE_EXIT if it should also leave the view, just + // exiting is identical to reverting + const val DONE_EXIT = 1 shl 0 + const val OPEN_EMAIL_MARKER = " <" + const val CLOSE_EMAIL_MARKER = ">" + const val INTENT_KEY_DETAIL_VIEW = "DETAIL_VIEW" + const val INTENT_KEY_VIEW_TYPE = "VIEW" + const val INTENT_VALUE_VIEW_TYPE_DAY = "DAY" + const val INTENT_KEY_HOME = "KEY_HOME" + val MONDAY_BEFORE_JULIAN_EPOCH: Int = Time.EPOCH_JULIAN_DAY - 3 + const val DECLINED_EVENT_ALPHA = 0x66 + const val DECLINED_EVENT_TEXT_ALPHA = 0xC0 + private const val SATURATION_ADJUST = 1.3f + private const val INTENSITY_ADJUST = 0.8f + + // Defines used by the DNA generation code + const val DAY_IN_MINUTES = 60 * 24 + const val WEEK_IN_MINUTES = DAY_IN_MINUTES * 7 + + // The work day is being counted as 6am to 8pm + var WORK_DAY_MINUTES = 14 * 60 + var WORK_DAY_START_MINUTES = 6 * 60 + var WORK_DAY_END_MINUTES = 20 * 60 + var WORK_DAY_END_LENGTH = 24 * 60 - WORK_DAY_END_MINUTES + var CONFLICT_COLOR = -0x1000000 + var mMinutesLoaded = false + const val YEAR_MIN = 1970 + const val YEAR_MAX = 2036 + + // The name of the shared preferences file. This name must be maintained for + // historical + // reasons, as it's what PreferenceManager assigned the first time the file + // was created. + const val SHARED_PREFS_NAME = "com.android.calendar_preferences" + const val KEY_QUICK_RESPONSES = "preferences_quick_responses" + const val KEY_ALERTS_VIBRATE_WHEN = "preferences_alerts_vibrateWhen" + const val APPWIDGET_DATA_TYPE = "vnd.android.data/update" + const val MACHINE_GENERATED_ADDRESS = "calendar.google.com" + private val mTZUtils: TimeZoneUtils? = TimeZoneUtils(SHARED_PREFS_NAME) + @JvmField var allowWeekForDetailView = false + internal var tardis: Long = 0 + private set + private var sVersion: String? = null + private val mWildcardPattern: Pattern = Pattern.compile("^.*$") + + /** + * A coordinate must be of the following form for Google Maps to correctly use it: + * Latitude, Longitude + * + * This may be in decimal form: + * Latitude: {-90 to 90} + * Longitude: {-180 to 180} + * + * Or, in degrees, minutes, and seconds: + * Latitude: {-90 to 90}° {0 to 59}' {0 to 59}" + * Latitude: {-180 to 180}° {0 to 59}' {0 to 59}" + * + or - degrees may also be represented with N or n, S or s for latitude, and with + * E or e, W or w for longitude, where the direction may either precede or follow the value. + * + * Some examples of coordinates that will be accepted by the regex: + * 37.422081°, -122.084576° + * 37.422081,-122.084576 + * +37°25'19.49", -122°5'4.47" + * 37°25'19.49"N, 122°5'4.47"W + * N 37° 25' 19.49", W 122° 5' 4.47" + */ + private const val COORD_DEGREES_LATITUDE = ("([-+NnSs]" + "(\\s)*)?" + + "[1-9]?[0-9](\u00B0)" + "(\\s)*" + + "([1-5]?[0-9]\')?" + "(\\s)*" + + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" + + "((\\s)*" + "[NnSs])?") + private const val COORD_DEGREES_LONGITUDE = ("([-+EeWw]" + "(\\s)*)?" + + "(1)?[0-9]?[0-9](\u00B0)" + "(\\s)*" + + "([1-5]?[0-9]\')?" + "(\\s)*" + + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" + + "((\\s)*" + "[EeWw])?") + private const val COORD_DEGREES_PATTERN = (COORD_DEGREES_LATITUDE + "(\\s)*" + "," + "(\\s)*" + + COORD_DEGREES_LONGITUDE) + private const val COORD_DECIMAL_LATITUDE = ("[+-]?" + + "[1-9]?[0-9]" + "(\\.[0-9]+)" + + "(\u00B0)?") + private const val COORD_DECIMAL_LONGITUDE = ("[+-]?" + + "(1)?[0-9]?[0-9]" + "(\\.[0-9]+)" + + "(\u00B0)?") + private const val COORD_DECIMAL_PATTERN = (COORD_DECIMAL_LATITUDE + "(\\s)*" + "," + "(\\s)*" + + COORD_DECIMAL_LONGITUDE) + private val COORD_PATTERN: Pattern = + Pattern.compile(COORD_DEGREES_PATTERN + "|" + COORD_DECIMAL_PATTERN) + private const val NANP_ALLOWED_SYMBOLS = "()+-*#." + private const val NANP_MIN_DIGITS = 7 + private const val NANP_MAX_DIGITS = 11 + + /** + * Returns whether the SDK is the KeyLimePie release or later. + */ + @JvmStatic fun isKeyLimePieOrLater(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + } + + /** + * Returns whether the SDK is the Jellybean release or later. + */ + @JvmStatic fun isJellybeanOrLater(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + } + + @JvmStatic fun getViewTypeFromIntentAndSharedPref(activity: Activity): Int { + val intent: Intent? = activity.getIntent() + val extras: Bundle? = intent?.getExtras() + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(activity) + if (TextUtils.equals(intent?.getAction(), Intent.ACTION_EDIT)) { + return ViewType.EDIT + } + if (extras != null) { + if (extras?.getBoolean(INTENT_KEY_DETAIL_VIEW, false)) { + // This is the "detail" view which is either agenda or day view + return prefs?.getInt( + GeneralPreferences.KEY_DETAILED_VIEW, + GeneralPreferences.DEFAULT_DETAILED_VIEW + ) as Int + } else if (INTENT_VALUE_VIEW_TYPE_DAY.equals(extras?.getString(INTENT_KEY_VIEW_TYPE))) { + // Not sure who uses this. This logic came from LaunchActivity + return ViewType.DAY + } + } + + // Default to the last view + return prefs?.getInt( + GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW + ) as Int + } + + /** + * Gets the intent action for telling the widget to update. + */ + @JvmStatic fun getWidgetUpdateAction(context: Context): String { + return context.getPackageName().toString() + ".APPWIDGET_UPDATE" + } + + /** + * Gets the intent action for telling the widget to update. + */ + @JvmStatic fun getWidgetScheduledUpdateAction(context: Context): String { + return context.getPackageName().toString() + ".APPWIDGET_SCHEDULED_UPDATE" + } + + /** + * Writes a new home time zone to the db. Updates the home time zone in the + * db asynchronously and updates the local cache. Sending a time zone of + * **tbd** will cause it to be set to the device's time zone. null or empty + * tz will be ignored. + * + * @param context The calling activity + * @param timeZone The time zone to set Calendar to, or **tbd** + */ + @JvmStatic fun setTimeZone(context: Context?, timeZone: String?) { + mTZUtils?.setTimeZone(context as Context, timeZone as String) + } + + /** + * Gets the time zone that Calendar should be displayed in This is a helper + * method to get the appropriate time zone for Calendar. If this is the + * first time this method has been called it will initiate an asynchronous + * query to verify that the data in preferences is correct. The callback + * supplied will only be called if this query returns a value other than + * what is stored in preferences and should cause the calling activity to + * refresh anything that depends on calling this method. + * + * @param context The calling activity + * @param callback The runnable that should execute if a query returns new + * values + * @return The string value representing the time zone Calendar should + * display + */ + @JvmStatic fun getTimeZone(context: Context?, callback: Runnable?): String? { + return mTZUtils?.getTimeZone(context as Context, callback) + } + + /** + * Formats a date or a time range according to the local conventions. + * + * @param context the context is required only if the time is shown + * @param startMillis the start time in UTC milliseconds + * @param endMillis the end time in UTC milliseconds + * @param flags a bit mask of options See [formatDateRange][DateUtils.formatDateRange] + * @return a string containing the formatted date/time range. + */ + @JvmStatic fun formatDateRange( + context: Context?, + startMillis: Long, + endMillis: Long, + flags: Int + ): String? { + return mTZUtils?.formatDateRange(context as Context, startMillis, endMillis, flags) + } + + @JvmStatic fun getDefaultVibrate(context: Context, prefs: SharedPreferences?): Boolean { + val vibrate: Boolean + if (prefs?.contains(KEY_ALERTS_VIBRATE_WHEN) == true) { + // Migrate setting to new 4.2 behavior + // + // silent and never -> off + // always -> on + val vibrateWhen: String? = prefs?.getString(KEY_ALERTS_VIBRATE_WHEN, null) + vibrate = vibrateWhen != null && vibrateWhen.equals( + context + .getString(R.string.prefDefault_alerts_vibrate_true) + ) + prefs?.edit().remove(KEY_ALERTS_VIBRATE_WHEN).commit() + Log.d( + TAG, "Migrating KEY_ALERTS_VIBRATE_WHEN(" + + vibrateWhen + ") to KEY_ALERTS_VIBRATE = " + vibrate + ) + } else { + vibrate = prefs?.getBoolean( + GeneralPreferences.KEY_ALERTS_VIBRATE, + false + ) as Boolean + } + return vibrate + } + + @JvmStatic fun getSharedPreference( + context: Context?, + key: String?, + defaultValue: Array<String>? + ): Array<String>? { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + val ss = prefs?.getStringSet(key, null) + if (ss != null) { + val strings = arrayOfNulls<String>(ss?.size) + return ss?.toTypedArray() + } + return defaultValue + } + + @JvmStatic fun getSharedPreference( + context: Context?, + key: String?, + defaultValue: String? + ): String? { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + return prefs?.getString(key, defaultValue) + } + + @JvmStatic fun getSharedPreference(context: Context?, key: String?, defaultValue: Int): Int { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + return prefs?.getInt(key, defaultValue) as Int + } + + @JvmStatic fun getSharedPreference( + context: Context?, + key: String?, + defaultValue: Boolean + ): Boolean { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + return prefs?.getBoolean(key, defaultValue) as Boolean + } + + /** + * Asynchronously sets the preference with the given key to the given value + * + * @param context the context to use to get preferences from + * @param key the key of the preference to set + * @param value the value to set + */ + @JvmStatic fun setSharedPreference(context: Context?, key: String?, value: String?) { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + prefs?.edit()?.putString(key, value)?.apply() + } + + @JvmStatic fun setSharedPreference(context: Context?, key: String?, values: Array<String?>) { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + val set: LinkedHashSet<String?> = LinkedHashSet<String?>() + for (value in values) { + set.add(value) + } + prefs?.edit()?.putStringSet(key, set)?.apply() + } + + internal fun tardis() { + tardis = System.currentTimeMillis() + } + + @JvmStatic fun setSharedPreference(context: Context?, key: String?, value: Boolean) { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + val editor: SharedPreferences.Editor? = prefs?.edit() + editor?.putBoolean(key, value) + editor?.apply() + } + + @JvmStatic fun setSharedPreference(context: Context?, key: String?, value: Int) { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + val editor: SharedPreferences.Editor? = prefs?.edit() + editor?.putInt(key, value) + editor?.apply() + } + + @JvmStatic fun removeSharedPreference(context: Context?, key: String?) { + val prefs: SharedPreferences? = context?.getSharedPreferences( + GeneralPreferences.SHARED_PREFS_NAME, Context.MODE_PRIVATE + ) + prefs?.edit()?.remove(key)?.apply() + } + + /** + * Save default agenda/day/week/month view for next time + * + * @param context + * @param viewId [CalendarController.ViewType] + */ + @JvmStatic fun setDefaultView(context: Context?, viewId: Int) { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + val editor: SharedPreferences.Editor? = prefs?.edit() + var validDetailView = false + validDetailView = + if (allowWeekForDetailView && viewId == CalendarController.ViewType.WEEK) { + true + } else { + (viewId == CalendarController.ViewType.AGENDA || + viewId == CalendarController.ViewType.DAY) + } + if (validDetailView) { + // Record the detail start view + editor?.putInt(GeneralPreferences.KEY_DETAILED_VIEW, viewId) + } + + // Record the (new) start view + editor?.putInt(GeneralPreferences.KEY_START_VIEW, viewId) + editor?.apply() + } + + @JvmStatic fun matrixCursorFromCursor(cursor: Cursor?): MatrixCursor? { + if (cursor == null) { + return null + } + var columnNames: Array<String?> = cursor.getColumnNames() + if (columnNames == null) { + columnNames = arrayOf() + } + val newCursor = MatrixCursor(columnNames) + val numColumns: Int = cursor.getColumnCount() + val data = arrayOfNulls<String>(numColumns) + cursor.moveToPosition(-1) + while (cursor.moveToNext()) { + for (i in 0 until numColumns) { + data[i] = cursor.getString(i) + } + newCursor.addRow(data) + } + return newCursor + } + + /** + * Compares two cursors to see if they contain the same data. + * + * @return Returns true of the cursors contain the same data and are not + * null, false otherwise + */ + @JvmStatic fun compareCursors(c1: Cursor?, c2: Cursor?): Boolean { + if (c1 == null || c2 == null) { + return false + } + val numColumns: Int = c1.getColumnCount() + if (numColumns != c2.getColumnCount()) { + return false + } + if (c1.getCount() !== c2.getCount()) { + return false + } + c1.moveToPosition(-1) + c2.moveToPosition(-1) + while (c1.moveToNext() && c2.moveToNext()) { + for (i in 0 until numColumns) { + if (!TextUtils.equals(c1.getString(i), c2.getString(i))) { + return false + } + } + } + return true + } + + /** + * If the given intent specifies a time (in milliseconds since the epoch), + * then that time is returned. Otherwise, the current time is returned. + */ + @JvmStatic fun timeFromIntentInMillis(intent: Intent?): Long? { + // If the time was specified, then use that. Otherwise, use the current + // time. + val data: Uri? = intent?.getData() + var millis: Long? = intent?.getLongExtra(EXTRA_EVENT_BEGIN_TIME, -1)?.toLong() + if (millis == -1L && data != null && data?.isHierarchical()) { + val path: List<String> = data?.getPathSegments() as List<String> + if (path.size == 2 && path[0].equals("time")) { + try { + millis = (data?.getLastPathSegment()?.toLong()) + } catch (e: NumberFormatException) { + Log.i( + "Calendar", "timeFromIntentInMillis: Data existed but no valid time " + + "found. Using current time." + ) + } + } + } + if ((millis ?: 0L) <= 0) { + millis = System.currentTimeMillis() + } + return millis + } + + /** + * Formats the given Time object so that it gives the month and year (for + * example, "September 2007"). + * + * @param time the time to format + * @return the string containing the weekday and the date + */ + @JvmStatic fun formatMonthYear(context: Context?, time: Time): String? { + val flags: Int = (DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_MONTH_DAY + or DateUtils.FORMAT_SHOW_YEAR) + val millis: Long = time.toMillis(true) + return formatDateRange(context, millis, millis, flags) + } + + /** + * Returns a list joined together by the provided delimiter, for example, + * ["a", "b", "c"] could be joined into "a,b,c" + * + * @param things the things to join together + * @param delim the delimiter to use + * @return a string contained the things joined together + */ + @JvmStatic fun join(things: List<*>, delim: String?): String { + val builder = StringBuilder() + var first = true + for (thing in things) { + if (first) { + first = false + } else { + builder.append(delim) + } + builder.append(thing.toString()) + } + return builder.toString() + } + + /** + * Returns the week since [Time.EPOCH_JULIAN_DAY] (Jan 1, 1970) + * adjusted for first day of week. + * + * This takes a julian day and the week start day and calculates which + * week since [Time.EPOCH_JULIAN_DAY] that day occurs in, starting + * at 0. *Do not* use this to compute the ISO week number for the year. + * + * @param julianDay The julian day to calculate the week number for + * @param firstDayOfWeek Which week day is the first day of the week, + * see [Time.SUNDAY] + * @return Weeks since the epoch + */ + @JvmStatic fun getWeeksSinceEpochFromJulianDay(julianDay: Int, firstDayOfWeek: Int): Int { + var diff: Int = Time.THURSDAY - firstDayOfWeek + if (diff < 0) { + diff += 7 + } + val refDay: Int = Time.EPOCH_JULIAN_DAY - diff + return (julianDay - refDay) / 7 + } + + /** + * Takes a number of weeks since the epoch and calculates the Julian day of + * the Monday for that week. + * + * This assumes that the week containing the [Time.EPOCH_JULIAN_DAY] + * is considered week 0. It returns the Julian day for the Monday + * `week` weeks after the Monday of the week containing the epoch. + * + * @param week Number of weeks since the epoch + * @return The julian day for the Monday of the given week since the epoch + */ + @JvmStatic fun getJulianMondayFromWeeksSinceEpoch(week: Int): Int { + return MONDAY_BEFORE_JULIAN_EPOCH + week * 7 + } + + /** + * Get first day of week as android.text.format.Time constant. + * + * @return the first day of week in android.text.format.Time + */ + @JvmStatic fun getFirstDayOfWeek(context: Context?): Int { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + val pref: String? = prefs?.getString( + GeneralPreferences.KEY_WEEK_START_DAY, GeneralPreferences.WEEK_START_DEFAULT + ) + val startDay: Int + startDay = if (GeneralPreferences.WEEK_START_DEFAULT.equals(pref)) { + Calendar.getInstance().getFirstDayOfWeek() + } else { + Integer.parseInt(pref) + } + return if (startDay == Calendar.SATURDAY) { + Time.SATURDAY + } else if (startDay == Calendar.MONDAY) { + Time.MONDAY + } else { + Time.SUNDAY + } + } + + /** + * Get first day of week as java.util.Calendar constant. + * + * @return the first day of week as a java.util.Calendar constant + */ + @JvmStatic fun getFirstDayOfWeekAsCalendar(context: Context?): Int { + return convertDayOfWeekFromTimeToCalendar(getFirstDayOfWeek(context)) + } + + /** + * Converts the day of the week from android.text.format.Time to java.util.Calendar + */ + @JvmStatic fun convertDayOfWeekFromTimeToCalendar(timeDayOfWeek: Int): Int { + return when (timeDayOfWeek) { + Time.MONDAY -> Calendar.MONDAY + Time.TUESDAY -> Calendar.TUESDAY + Time.WEDNESDAY -> Calendar.WEDNESDAY + Time.THURSDAY -> Calendar.THURSDAY + Time.FRIDAY -> Calendar.FRIDAY + Time.SATURDAY -> Calendar.SATURDAY + Time.SUNDAY -> Calendar.SUNDAY + else -> throw IllegalArgumentException( + "Argument must be between Time.SUNDAY and " + + "Time.SATURDAY" + ) + } + } + + /** + * @return true when week number should be shown. + */ + @JvmStatic fun getShowWeekNumber(context: Context?): Boolean { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + return prefs?.getBoolean( + GeneralPreferences.KEY_SHOW_WEEK_NUM, GeneralPreferences.DEFAULT_SHOW_WEEK_NUM + ) as Boolean + } + + /** + * @return true when declined events should be hidden. + */ + @JvmStatic fun getHideDeclinedEvents(context: Context?): Boolean { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + return prefs?.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, false) as Boolean + } + + @JvmStatic fun getDaysPerWeek(context: Context?): Int { + val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context) + return prefs?.getInt(GeneralPreferences.KEY_DAYS_PER_WEEK, 7) as Int + } + + /** + * Determine whether the column position is Saturday or not. + * + * @param column the column position + * @param firstDayOfWeek the first day of week in android.text.format.Time + * @return true if the column is Saturday position + */ + @JvmStatic fun isSaturday(column: Int, firstDayOfWeek: Int): Boolean { + return (firstDayOfWeek == Time.SUNDAY && column == 6 || + firstDayOfWeek == Time.MONDAY && column == 5 || + firstDayOfWeek == Time.SATURDAY && column == 0) + } + + /** + * Determine whether the column position is Sunday or not. + * + * @param column the column position + * @param firstDayOfWeek the first day of week in android.text.format.Time + * @return true if the column is Sunday position + */ + @JvmStatic fun isSunday(column: Int, firstDayOfWeek: Int): Boolean { + return (firstDayOfWeek == Time.SUNDAY && column == 0 || + firstDayOfWeek == Time.MONDAY && column == 6 || + firstDayOfWeek == Time.SATURDAY && column == 1) + } + + /** + * Convert given UTC time into current local time. This assumes it is for an + * allday event and will adjust the time to be on a midnight boundary. + * + * @param recycle Time object to recycle, otherwise null. + * @param utcTime Time to convert, in UTC. + * @param tz The time zone to convert this time to. + */ + @JvmStatic fun convertAlldayUtcToLocal(recycle: Time?, utcTime: Long, tz: String): Long { + var recycle: Time? = recycle + if (recycle == null) { + recycle = Time() + } + recycle.timezone = Time.TIMEZONE_UTC + recycle.set(utcTime) + recycle.timezone = tz + return recycle.normalize(true) + } + + @JvmStatic fun convertAlldayLocalToUTC(recycle: Time?, localTime: Long, tz: String): Long { + var recycle: Time? = recycle + if (recycle == null) { + recycle = Time() + } + recycle.timezone = tz + recycle.set(localTime) + recycle.timezone = Time.TIMEZONE_UTC + return recycle.normalize(true) + } + + /** + * Finds and returns the next midnight after "theTime" in milliseconds UTC + * + * @param recycle - Time object to recycle, otherwise null. + * @param theTime - Time used for calculations (in UTC) + * @param tz The time zone to convert this time to. + */ + @JvmStatic fun getNextMidnight(recycle: Time?, theTime: Long, tz: String): Long { + var recycle: Time? = recycle + if (recycle == null) { + recycle = Time() + } + recycle.timezone = tz + recycle.set(theTime) + recycle.monthDay++ + recycle.hour = 0 + recycle.minute = 0 + recycle.second = 0 + return recycle.normalize(true) + } + + @JvmStatic fun setAllowWeekForDetailView(allowWeekView: Boolean) { + this.allowWeekForDetailView = allowWeekView + } + + @JvmStatic fun getAllowWeekForDetailView(): Boolean { + return this.allowWeekForDetailView + } + + @JvmStatic fun getConfigBool(c: Context, key: Int): Boolean { + return c.getResources().getBoolean(key) + } + + /** + * For devices with Jellybean or later, darkens the given color to ensure that white text is + * clearly visible on top of it. For devices prior to Jellybean, does nothing, as the + * sync adapter handles the color change. + * + * @param color + */ + @JvmStatic fun getDisplayColorFromColor(color: Int): Int { + if (!isJellybeanOrLater()) { + return color + } + val hsv = FloatArray(3) + Color.colorToHSV(color, hsv) + hsv[1] = Math.min(hsv[1] * SATURATION_ADJUST, 1.0f) + hsv[2] = hsv[2] * INTENSITY_ADJUST + return Color.HSVToColor(hsv) + } + + // This takes a color and computes what it would look like blended with + // white. The result is the color that should be used for declined events. + @JvmStatic fun getDeclinedColorFromColor(color: Int): Int { + val bg = -0x1 + val a = DECLINED_EVENT_ALPHA + val r = (color and 0x00ff0000) * a + (bg and 0x00ff0000) * (0xff - a) and -0x1000000 + val g = (color and 0x0000ff00) * a + (bg and 0x0000ff00) * (0xff - a) and 0x00ff0000 + val b = (color and 0x000000ff) * a + (bg and 0x000000ff) * (0xff - a) and 0x0000ff00 + return -0x1000000 or (r or g or b shr 8) + } + + @JvmStatic fun trySyncAndDisableUpgradeReceiver(context: Context?) { + val pm: PackageManager? = context?.getPackageManager() + val upgradeComponent = ComponentName(context as Context, UpgradeReceiver::class.java) + if (pm?.getComponentEnabledSetting(upgradeComponent) === + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + ) { + // The upgrade receiver has been disabled, which means this code has been run before, + // so no need to sync. + return + } + val extras = Bundle() + extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) + ContentResolver.requestSync( + null /* no account */, + Calendars.CONTENT_URI.getAuthority(), + extras + ) + + // Now unregister the receiver so that we won't continue to sync every time. + pm?.setComponentEnabledSetting( + upgradeComponent, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP + ) + } + + /** + * Converts a list of events to a list of segments to draw. Assumes list is + * ordered by start time of the events. The function processes events for a + * range of days from firstJulianDay to firstJulianDay + dayXs.length - 1. + * The algorithm goes over all the events and creates a set of segments + * ordered by start time. This list of segments is then converted into a + * HashMap of strands which contain the draw points and are organized by + * color. The strands can then be drawn by setting the paint color to each + * strand's color and calling drawLines on its set of points. The points are + * set up using the following parameters. + * + * * Events between midnight and WORK_DAY_START_MINUTES are compressed + * into the first 1/8th of the space between top and bottom. + * * Events between WORK_DAY_END_MINUTES and the following midnight are + * compressed into the last 1/8th of the space between top and bottom + * * Events between WORK_DAY_START_MINUTES and WORK_DAY_END_MINUTES use + * the remaining 3/4ths of the space + * * All segments drawn will maintain at least minPixels height, except + * for conflicts in the first or last 1/8th, which may be smaller + * + * + * @param firstJulianDay The julian day of the first day of events + * @param events A list of events sorted by start time + * @param top The lowest y value the dna should be drawn at + * @param bottom The highest y value the dna should be drawn at + * @param dayXs An array of x values to draw the dna at, one for each day + * @param conflictColor the color to use for conflicts + * @return + */ + @JvmStatic fun createDNAStrands( + firstJulianDay: Int, + events: ArrayList<Event?>?, + top: Int, + bottom: Int, + minPixels: Int, + dayXs: IntArray?, + context: Context? + ): HashMap<Int, DNAStrand>? { + if (!mMinutesLoaded) { + if (context == null) { + Log.wtf(TAG, "No context and haven't loaded parameters yet! Can't create DNA.") + } + val res: Resources? = context?.getResources() + CONFLICT_COLOR = res?.getColor(R.color.month_dna_conflict_time_color) as Int + WORK_DAY_START_MINUTES = res?.getInteger(R.integer.work_start_minutes) as Int + WORK_DAY_END_MINUTES = res?.getInteger(R.integer.work_end_minutes) as Int + WORK_DAY_END_LENGTH = DAY_IN_MINUTES - WORK_DAY_END_MINUTES + WORK_DAY_MINUTES = WORK_DAY_END_MINUTES - WORK_DAY_START_MINUTES + mMinutesLoaded = true + } + if (events == null || events.isEmpty() || dayXs == null || dayXs.size < 1 || + bottom - top < 8 || minPixels < 0) { + Log.e( + TAG, + "Bad values for createDNAStrands! events:" + events + " dayXs:" + + Arrays.toString(dayXs) + " bot-top:" + (bottom - top) + " minPixels:" + + minPixels + ) + return null + } + val segments: LinkedList<DNASegment> = LinkedList<DNASegment>() + val strands: HashMap<Int, DNAStrand> = HashMap<Int, DNAStrand>() + // add a black strand by default, other colors will get added in + // the loop + val blackStrand = DNAStrand() + blackStrand.color = CONFLICT_COLOR + strands.put(CONFLICT_COLOR, blackStrand) + // the min length is the number of minutes that will occupy + // MIN_SEGMENT_PIXELS in the 'work day' time slot. This computes the + // minutes/pixel * minpx where the number of pixels are 3/4 the total + // dna height: 4*(mins/(px * 3/4)) + val minMinutes = minPixels * 4 * WORK_DAY_MINUTES / (3 * (bottom - top)) + + // There are slightly fewer than half as many pixels in 1/6 the space, + // so round to 2.5x for the min minutes in the non-work area + val minOtherMinutes = minMinutes * 5 / 2 + val lastJulianDay = firstJulianDay + dayXs.size - 1 + val event = Event() + // Go through all the events for the week + for (currEvent in events) { + // if this event is outside the weeks range skip it + if (currEvent != null && + (currEvent.endDay < firstJulianDay || currEvent.startDay > lastJulianDay)) { + continue + } + if (currEvent?.drawAsAllday() == true) { + addAllDayToStrands(currEvent, strands, firstJulianDay, dayXs.size) + continue + } + // Copy the event over so we can clip its start and end to our range + currEvent?.copyTo(event) + if (event.startDay < firstJulianDay) { + event.startDay = firstJulianDay + event.startTime = 0 + } + // If it starts after the work day make sure the start is at least + // minPixels from midnight + if (event.startTime > DAY_IN_MINUTES - minOtherMinutes) { + event.startTime = DAY_IN_MINUTES - minOtherMinutes + } + if (event.endDay > lastJulianDay) { + event.endDay = lastJulianDay + event.endTime = DAY_IN_MINUTES - 1 + } + // If the end time is before the work day make sure it ends at least + // minPixels after midnight + if (event.endTime < minOtherMinutes) { + event.endTime = minOtherMinutes + } + // If the start and end are on the same day make sure they are at + // least minPixels apart. This only needs to be done for times + // outside the work day as the min distance for within the work day + // is enforced in the segment code. + if (event.startDay === event.endDay && + event.endTime - event.startTime < minOtherMinutes + ) { + // If it's less than minPixels in an area before the work + // day + if (event.startTime < WORK_DAY_START_MINUTES) { + // extend the end to the first easy guarantee that it's + // minPixels + event.endTime = Math.min( + event.startTime + minOtherMinutes, + WORK_DAY_START_MINUTES + minMinutes + ) + // if it's in the area after the work day + } else if (event.endTime > WORK_DAY_END_MINUTES) { + // First try shifting the end but not past midnight + event.endTime = Math.min(event.endTime + minOtherMinutes, DAY_IN_MINUTES - 1) + // if it's still too small move the start back + if (event.endTime - event.startTime < minOtherMinutes) { + event.startTime = event.endTime - minOtherMinutes + } + } + } + + // This handles adding the first segment + if (segments.size == 0) { + addNewSegment(segments, event, strands, firstJulianDay, 0, minMinutes) + continue + } + // Now compare our current start time to the end time of the last + // segment in the list + val lastSegment: DNASegment = segments.getLast() + var startMinute: Int = + (event.startDay - firstJulianDay) * DAY_IN_MINUTES + event.startTime + var endMinute: Int = Math.max( + (event.endDay - firstJulianDay) * DAY_IN_MINUTES + + event.endTime, startMinute + minMinutes + ) + if (startMinute < 0) { + startMinute = 0 + } + if (endMinute >= WEEK_IN_MINUTES) { + endMinute = WEEK_IN_MINUTES - 1 + } + // If we start before the last segment in the list ends we need to + // start going through the list as this may conflict with other + // events + if (startMinute < lastSegment.endMinute) { + var i: Int = segments.size + // find the last segment this event intersects with + while (--i >= 0 && endMinute < segments.get(i).startMinute) {} + + var currSegment: DNASegment = DNASegment() + // for each segment this event intersects with + while (i >= 0 && startMinute <= segments.get(i) + .also { currSegment = it }.endMinute) { + + // if the segment is already a conflict ignore it + if (currSegment.color == CONFLICT_COLOR) { + i-- + continue + } + // if the event ends before the segment and wouldn't create + // a segment that is too small split off the right side + if (endMinute < currSegment.endMinute - minMinutes) { + val rhs = DNASegment() + rhs.endMinute = currSegment.endMinute + rhs.color = currSegment.color + rhs.startMinute = endMinute + 1 + rhs.day = currSegment.day + currSegment.endMinute = endMinute + segments.add(i + 1, rhs) + // Equivalent to strands.get(rhs.color)?.count++ + // but there is no null safe invocation for ++ + strands.get(rhs.color)?.count = strands.get(rhs.color)?.count?.inc() as Int + if (DEBUG) { + Log.d( + TAG, "Added rhs, curr:" + currSegment.toString() + " i:" + + segments.get(i).toString() + ) + } + } + // if the event starts after the segment and wouldn't create + // a segment that is too small split off the left side + if (startMinute > currSegment.startMinute + minMinutes) { + val lhs = DNASegment() + lhs.startMinute = currSegment.startMinute + lhs.color = currSegment.color + lhs.endMinute = startMinute - 1 + lhs.day = currSegment.day + currSegment.startMinute = startMinute + // increment i so that we are at the right position when + // referencing the segments to the right and left of the + // current segment. + segments.add(i++, lhs) + strands.get(lhs.color)?.count = strands.get(lhs.color)?.count?.inc() as Int + if (DEBUG) { + Log.d( + TAG, "Added lhs, curr:" + currSegment.toString() + " i:" + + segments.get(i).toString() + ) + } + } + // if the right side is black merge this with the segment to + // the right if they're on the same day and overlap + if (i + 1 < segments.size) { + val rhs: DNASegment = segments.get(i + 1) + if (rhs.color == CONFLICT_COLOR && currSegment.day == rhs.day && + rhs.startMinute <= currSegment.endMinute + 1) { + rhs.startMinute = Math.min(currSegment.startMinute, rhs.startMinute) + segments.remove(currSegment) + strands.get(currSegment.color)?.count = + strands.get(currSegment.color)?.count?.dec() as Int + // point at the new current segment + currSegment = rhs + } + } + // if the left side is black merge this with the segment to + // the left if they're on the same day and overlap + if (i - 1 >= 0) { + val lhs: DNASegment = segments.get(i - 1) + if (lhs.color == CONFLICT_COLOR && currSegment.day == lhs.day && + lhs.endMinute >= currSegment.startMinute - 1) { + lhs.endMinute = Math.max(currSegment.endMinute, lhs.endMinute) + segments.remove(currSegment) + strands.get(currSegment.color)?.count = + strands.get(currSegment.color)?.count?.dec() as Int + // point at the new current segment + currSegment = lhs + // point i at the new current segment in case new + // code is added + i-- + } + } + // if we're still not black, decrement the count for the + // color being removed, change this to black, and increment + // the black count + if (currSegment.color != CONFLICT_COLOR) { + strands.get(currSegment.color)?.count = + strands.get(currSegment.color)?.count?.dec() as Int + currSegment.color = CONFLICT_COLOR + strands.get(CONFLICT_COLOR)?.count = + strands.get(CONFLICT_COLOR)?.count?.inc() as Int + } + i-- + } + } + // If this event extends beyond the last segment add a new segment + if (endMinute > lastSegment.endMinute) { + addNewSegment( + segments, event, strands, firstJulianDay, lastSegment.endMinute, + minMinutes + ) + } + } + weaveDNAStrands(segments, firstJulianDay, strands, top, bottom, dayXs) + return strands + } + + // This figures out allDay colors as allDay events are found + private fun addAllDayToStrands( + event: Event?, + strands: HashMap<Int, DNAStrand>, + firstJulianDay: Int, + numDays: Int + ) { + val strand = getOrCreateStrand(strands, CONFLICT_COLOR) + // if we haven't initialized the allDay portion create it now + if (strand?.allDays == null) { + strand?.allDays = IntArray(numDays) + } + + // For each day this event is on update the color + val end: Int = Math.min((event?.endDay ?: 0) - firstJulianDay, numDays - 1) + for (i in Math.max((event?.startDay ?: 0) - firstJulianDay, 0)..end) { + if (strand?.allDays!![i] != 0) { + // if this day already had a color, it is now a conflict + strand?.allDays!![i] = CONFLICT_COLOR + } else { + // else it's just the color of the event + strand?.allDays!![i] = event?.color as Int + } + } + } + + // This processes all the segments, sorts them by color, and generates a + // list of points to draw + private fun weaveDNAStrands( + segments: LinkedList<DNASegment>, + firstJulianDay: Int, + strands: HashMap<Int, DNAStrand>, + top: Int, + bottom: Int, + dayXs: IntArray + ) { + // First, get rid of any colors that ended up with no segments + val strandIterator = strands.values.iterator() + while (strandIterator.hasNext()) { + val strand = strandIterator.next() + if (strand?.count < 1 && strand.allDays == null) { + strandIterator.remove() + continue + } + strand.points = FloatArray(strand.count * 4) + strand.position = 0 + } + // Go through each segment and compute its points + for (segment in segments) { + // Add the points to the strand of that color + val strand: DNAStrand? = strands.get(segment.color) + val dayIndex = segment.day - firstJulianDay + val dayStartMinute = segment.startMinute % DAY_IN_MINUTES + val dayEndMinute = segment.endMinute % DAY_IN_MINUTES + val height = bottom - top + val workDayHeight = height * 3 / 4 + val remainderHeight = (height - workDayHeight) / 2 + val x = dayXs[dayIndex] + var y0 = 0 + var y1 = 0 + y0 = top + getPixelOffsetFromMinutes(dayStartMinute, workDayHeight, remainderHeight) + y1 = top + getPixelOffsetFromMinutes(dayEndMinute, workDayHeight, remainderHeight) + if (DEBUG) { + Log.d( + TAG, + "Adding " + Integer.toHexString(segment.color).toString() + " at x,y0,y1: " + x + .toString() + " " + y0.toString() + " " + y1.toString() + + " for " + dayStartMinute.toString() + " " + dayEndMinute + ) + } + strand?.points!![strand?.position] = x.toFloat() + strand?.position = strand?.position?.inc() as Int + + strand?.points!![strand?.position] = y0.toFloat() + strand?.position = strand?.position?.inc() as Int + + strand?.points!![strand?.position] = x.toFloat() + strand?.position = strand?.position.inc() as Int + + strand?.points!![strand?.position] = y1.toFloat() + strand?.position = strand?.position.inc() as Int + } + } + + /** + * Compute a pixel offset from the top for a given minute from the work day + * height and the height of the top area. + */ + private fun getPixelOffsetFromMinutes( + minute: Int, + workDayHeight: Int, + remainderHeight: Int + ): Int { + val y: Int + if (minute < WORK_DAY_START_MINUTES) { + y = minute * remainderHeight / WORK_DAY_START_MINUTES + } else if (minute < WORK_DAY_END_MINUTES) { + y = remainderHeight + (minute - WORK_DAY_START_MINUTES) * + workDayHeight / WORK_DAY_MINUTES + } else { + y = remainderHeight + workDayHeight + + (minute - WORK_DAY_END_MINUTES) * remainderHeight / WORK_DAY_END_LENGTH + } + return y + } + + /** + * Add a new segment based on the event provided. This will handle splitting + * segments across day boundaries and ensures a minimum size for segments. + */ + private fun addNewSegment( + segments: LinkedList<DNASegment>, + event: Event, + strands: HashMap<Int, DNAStrand>, + firstJulianDay: Int, + minStart: Int, + minMinutes: Int + ) { + var event: Event = event + var minStart = minStart + if (event.startDay > event.endDay) { + Log.wtf(TAG, "Event starts after it ends: " + event.toString()) + } + // If this is a multiday event split it up by day + if (event.startDay !== event.endDay) { + val lhs = Event() + lhs.color = event.color + lhs.startDay = event.startDay + // the first day we want the start time to be the actual start time + lhs.startTime = event.startTime + lhs.endDay = lhs.startDay + lhs.endTime = DAY_IN_MINUTES - 1 + // Nearly recursive iteration! + while (lhs.startDay !== event.endDay) { + addNewSegment(segments, lhs, strands, firstJulianDay, minStart, minMinutes) + // The days in between are all day, even though that shouldn't + // actually happen due to the allday filtering + lhs.startDay++ + lhs.endDay = lhs.startDay + lhs.startTime = 0 + minStart = 0 + } + // The last day we want the end time to be the actual end time + lhs.endTime = event.endTime + event = lhs + } + // Create the new segment and compute its fields + val segment = DNASegment() + val dayOffset: Int = (event.startDay - firstJulianDay) * DAY_IN_MINUTES + val endOfDay = dayOffset + DAY_IN_MINUTES - 1 + // clip the start if needed + segment.startMinute = Math.max(dayOffset + event.startTime, minStart) + // and extend the end if it's too small, but not beyond the end of the + // day + val minEnd: Int = Math.min(segment.startMinute + minMinutes, endOfDay) + segment.endMinute = Math.max(dayOffset + event.endTime, minEnd) + if (segment.endMinute > endOfDay) { + segment.endMinute = endOfDay + } + segment.color = event.color + segment.day = event.startDay + segments.add(segment) + // increment the count for the correct color or add a new strand if we + // don't have that color yet + val strand = getOrCreateStrand(strands, segment.color) + strand?.count + strand?.count = strand?.count?.inc() as Int + } + + /** + * Try to get a strand of the given color. Create it if it doesn't exist. + */ + private fun getOrCreateStrand(strands: HashMap<Int, DNAStrand>, color: Int): DNAStrand? { + var strand: DNAStrand? = strands.get(color) + if (strand == null) { + strand = DNAStrand() + strand?.color = color + strand?.count = 0 + strands?.put(strand?.color, strand) + } + return strand + } + + /** + * Sends an intent to launch the top level Calendar view. + * + * @param context + */ + @JvmStatic fun returnToCalendarHome(context: Context) { + val launchIntent = Intent(context, AllInOneActivity::class.java) + launchIntent.setAction(Intent.ACTION_DEFAULT) + launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + launchIntent.putExtra(INTENT_KEY_HOME, true) + context.startActivity(launchIntent) + } + + /** + * Given a context and a time in millis since unix epoch figures out the + * correct week of the year for that time. + * + * @param millisSinceEpoch + * @return + */ + @JvmStatic fun getWeekNumberFromTime(millisSinceEpoch: Long, context: Context?): Int { + val weekTime = Time(getTimeZone(context, null)) + weekTime.set(millisSinceEpoch) + weekTime.normalize(true) + val firstDayOfWeek = getFirstDayOfWeek(context) + // if the date is on Saturday or Sunday and the start of the week + // isn't Monday we may need to shift the date to be in the correct + // week + if (weekTime.weekDay === Time.SUNDAY && + (firstDayOfWeek == Time.SUNDAY || firstDayOfWeek == Time.SATURDAY) + ) { + weekTime.monthDay++ + weekTime.normalize(true) + } else if (weekTime.weekDay === Time.SATURDAY && firstDayOfWeek == Time.SATURDAY) { + weekTime.monthDay += 2 + weekTime.normalize(true) + } + return weekTime.getWeekNumber() + } + + /** + * Formats a day of the week string. This is either just the name of the day + * or a combination of yesterday/today/tomorrow and the day of the week. + * + * @param julianDay The julian day to get the string for + * @param todayJulianDay The julian day for today's date + * @param millis A utc millis since epoch time that falls on julian day + * @param context The calling context, used to get the timezone and do the + * formatting + * @return + */ + @JvmStatic fun getDayOfWeekString( + julianDay: Int, + todayJulianDay: Int, + millis: Long, + context: Context + ): String { + getTimeZone(context, null) + val flags: Int = DateUtils.FORMAT_SHOW_WEEKDAY + var dayViewText: String + dayViewText = if (julianDay == todayJulianDay) { + context.getString( + R.string.agenda_today, + mTZUtils?.formatDateRange(context, millis, millis, flags) + .toString() + ) + } else if (julianDay == todayJulianDay - 1) { + context.getString( + R.string.agenda_yesterday, + mTZUtils?.formatDateRange(context, millis, millis, flags) + .toString() + ) + } else if (julianDay == todayJulianDay + 1) { + context.getString( + R.string.agenda_tomorrow, + mTZUtils?.formatDateRange(context, millis, millis, flags) + .toString() + ) + } else { + mTZUtils?.formatDateRange(context, millis, millis, flags) + .toString() + } + dayViewText = dayViewText.toUpperCase() + return dayViewText + } + + // Calculate the time until midnight + 1 second and set the handler to + // do run the runnable + @JvmStatic fun setMidnightUpdater(h: Handler?, r: Runnable?, timezone: String?) { + if (h == null || r == null || timezone == null) { + return + } + val now: Long = System.currentTimeMillis() + val time = Time(timezone) + time.set(now) + val runInMillis: Long = ((24 * 3600 - time.hour * 3600 - time.minute * 60 - + time.second + 1) * 1000).toLong() + h.removeCallbacks(r) + h.postDelayed(r, runInMillis) + } + + // Stop the midnight update thread + @JvmStatic fun resetMidnightUpdater(h: Handler?, r: Runnable?) { + if (h == null || r == null) { + return + } + h.removeCallbacks(r) + } + + /** + * Returns a string description of the specified time interval. + */ + @JvmStatic fun getDisplayedDatetime( + startMillis: Long, + endMillis: Long, + currentMillis: Long, + localTimezone: String, + allDay: Boolean, + context: Context + ): String? { + // Configure date/time formatting. + val flagsDate: Int = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY + var flagsTime: Int = DateUtils.FORMAT_SHOW_TIME + if (DateFormat.is24HourFormat(context)) { + flagsTime = flagsTime or DateUtils.FORMAT_24HOUR + } + val currentTime = Time(localTimezone) + currentTime.set(currentMillis) + val resources: Resources = context.getResources() + var datetimeString: String? = null + if (allDay) { + // All day events require special timezone adjustment. + val localStartMillis = convertAlldayUtcToLocal(null, startMillis, localTimezone) + val localEndMillis = convertAlldayUtcToLocal(null, endMillis, localTimezone) + if (singleDayEvent(localStartMillis, localEndMillis, currentTime.gmtoff)) { + // If possible, use "Today" or "Tomorrow" instead of a full date string. + val todayOrTomorrow = isTodayOrTomorrow( + context.getResources(), + localStartMillis, currentMillis, currentTime.gmtoff + ) + if (TODAY == todayOrTomorrow) { + datetimeString = resources.getString(R.string.today) + } else if (TOMORROW == todayOrTomorrow) { + datetimeString = resources.getString(R.string.tomorrow) + } + } + if (datetimeString == null) { + // For multi-day allday events or single-day all-day events that are not + // today or tomorrow, use framework formatter. + val f = Formatter(StringBuilder(50), Locale.getDefault()) + datetimeString = DateUtils.formatDateRange( + context, f, startMillis, + endMillis, flagsDate, Time.TIMEZONE_UTC + ).toString() + } + } else { + datetimeString = if (singleDayEvent(startMillis, endMillis, currentTime.gmtoff)) { + // Format the time. + val timeString = formatDateRange( + context, startMillis, endMillis, + flagsTime + ) + + // If possible, use "Today" or "Tomorrow" instead of a full date string. + val todayOrTomorrow = isTodayOrTomorrow( + context.getResources(), startMillis, + currentMillis, currentTime.gmtoff + ) + if (TODAY == todayOrTomorrow) { + // Example: "Today at 1:00pm - 2:00 pm" + resources.getString( + R.string.today_at_time_fmt, + timeString + ) + } else if (TOMORROW == todayOrTomorrow) { + // Example: "Tomorrow at 1:00pm - 2:00 pm" + resources.getString( + R.string.tomorrow_at_time_fmt, + timeString + ) + } else { + // Format the full date. Example: "Thursday, April 12, 1:00pm - 2:00pm" + val dateString = formatDateRange( + context, startMillis, endMillis, + flagsDate + ) + resources.getString( + R.string.date_time_fmt, dateString, + timeString + ) + } + } else { + // For multiday events, shorten day/month names. + // Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm" + val flagsDatetime = flagsDate or flagsTime or DateUtils.FORMAT_ABBREV_MONTH or + DateUtils.FORMAT_ABBREV_WEEKDAY + formatDateRange( + context, startMillis, endMillis, + flagsDatetime + ) + } + } + return datetimeString + } + + /** + * Returns the timezone to display in the event info, if the local timezone is different + * from the event timezone. Otherwise returns null. + */ + @JvmStatic fun getDisplayedTimezone( + startMillis: Long, + localTimezone: String?, + eventTimezone: String? + ): String? { + var tzDisplay: String? = null + if (!TextUtils.equals(localTimezone, eventTimezone)) { + // Figure out if this is in DST + val tz: TimeZone = TimeZone.getTimeZone(localTimezone) + tzDisplay = if (tz == null || tz.getID().equals("GMT")) { + localTimezone + } else { + val startTime = Time(localTimezone) + startTime.set(startMillis) + tz.getDisplayName(startTime.isDst !== 0, TimeZone.SHORT) + } + } + return tzDisplay + } + + /** + * Returns whether the specified time interval is in a single day. + */ + private fun singleDayEvent(startMillis: Long, endMillis: Long, localGmtOffset: Long): Boolean { + if (startMillis == endMillis) { + return true + } + + // An event ending at midnight should still be a single-day event, so check + // time end-1. + val startDay: Int = Time.getJulianDay(startMillis, localGmtOffset) + val endDay: Int = Time.getJulianDay(endMillis - 1, localGmtOffset) + return startDay == endDay + } + + // Using int constants as a return value instead of an enum to minimize resources. + private const val TODAY = 1 + private const val TOMORROW = 2 + private const val NONE = 0 + + /** + * Returns TODAY or TOMORROW if applicable. Otherwise returns NONE. + */ + private fun isTodayOrTomorrow( + r: Resources, + dayMillis: Long, + currentMillis: Long, + localGmtOffset: Long + ): Int { + val startDay: Int = Time.getJulianDay(dayMillis, localGmtOffset) + val currentDay: Int = Time.getJulianDay(currentMillis, localGmtOffset) + val days = startDay - currentDay + return if (days == 1) { + TOMORROW + } else if (days == 0) { + TODAY + } else { + NONE + } + } + + /** + * Inserts a drawable with today's day into the today's icon in the option menu + * @param icon - today's icon from the options menu + */ + @JvmStatic fun setTodayIcon(icon: LayerDrawable, c: Context?, timezone: String?) { + val today: DayOfMonthDrawable + + // Reuse current drawable if possible + val currentDrawable: Drawable? = icon.findDrawableByLayerId(R.id.today_icon_day) + if (currentDrawable != null && currentDrawable is DayOfMonthDrawable) { + today = currentDrawable as DayOfMonthDrawable + } else { + today = DayOfMonthDrawable(c as Context) + } + // Set the day and update the icon + val now = Time(timezone) + now.setToNow() + now.normalize(false) + today.setDayOfMonth(now.monthDay) + icon.mutate() + icon.setDrawableByLayerId(R.id.today_icon_day, today) + } + + /** + * Get a list of quick responses used for emailing guests from the + * SharedPreferences. If not are found, get the hard coded ones that shipped + * with the app + * + * @param context + * @return a list of quick responses. + */ + fun getQuickResponses(context: Context): Array<String> { + var s = getSharedPreference(context, KEY_QUICK_RESPONSES, null as Array<String>?) + if (s == null) { + s = context.getResources().getStringArray(R.array.quick_response_defaults) + } + return s + } + + /** + * Return the app version code. + */ + fun getVersionCode(context: Context): String? { + if (sVersion == null) { + try { + sVersion = context.getPackageManager().getPackageInfo( + context.getPackageName(), 0 + ).versionName + } catch (e: PackageManager.NameNotFoundException) { + // Can't find version; just leave it blank. + Log.e(TAG, "Error finding package " + context.getApplicationInfo().packageName) + } + } + return sVersion + } + + // A single strand represents one color of events. Events are divided up by + // color to make them convenient to draw. The black strand is special in + // that it holds conflicting events as well as color settings for allday on + // each day. + class DNAStrand { + @JvmField var points: FloatArray? = null + @JvmField var allDays: IntArray? = null // color for the allday, 0 means no event + @JvmField var position = 0 + @JvmField var color = 0 + @JvmField var count = 0 + } + + // A segment is a single continuous length of time occupied by a single + // color. Segments should never span multiple days. + private class DNASegment { + var startMinute = 0 // in minutes since the start of the week = + var endMinute = 0 + var color = 0 // Calendar color or black for conflicts = + var day = 0 // quick reference to the day this segment is on = + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/alerts/AlarmManagerInterface.java b/src/com/android/calendar/alerts/AlarmManagerInterface.kt index 3c66434d..be9d86f2 100644 --- a/src/com/android/calendar/alerts/AlarmManagerInterface.java +++ b/src/com/android/calendar/alerts/AlarmManagerInterface.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * Copyright (C) 2021 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. @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.calendar.alerts -package com.android.calendar.alerts; - -import android.app.PendingIntent; +import android.app.PendingIntent /** * AlarmManager abstracted to an interface for testability. */ -public interface AlarmManagerInterface { - public void set(int type, long triggerAtMillis, PendingIntent operation); -} +interface AlarmManagerInterface { + operator fun set(type: Int, triggerAtMillis: Long, operation: PendingIntent?) +}
\ No newline at end of file diff --git a/src/com/android/calendar/alerts/AlarmScheduler.java b/src/com/android/calendar/alerts/AlarmScheduler.java deleted file mode 100644 index 97828229..00000000 --- a/src/com/android/calendar/alerts/AlarmScheduler.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * 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.calendar.alerts; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.provider.CalendarContract; -import android.provider.CalendarContract.Events; -import android.provider.CalendarContract.Instances; -import android.provider.CalendarContract.Reminders; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; - -import com.android.calendar.Utils; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Schedules the next EVENT_REMINDER_APP broadcast with AlarmManager, by querying the events - * and reminders tables for the next upcoming alert. - */ -public class AlarmScheduler { - private static final String TAG = "AlarmScheduler"; - - private static final String INSTANCES_WHERE = Events.VISIBLE + "=? AND " - + Instances.BEGIN + ">=? AND " + Instances.BEGIN + "<=? AND " - + Events.ALL_DAY + "=?"; - static final String[] INSTANCES_PROJECTION = new String[] { - Instances.EVENT_ID, - Instances.BEGIN, - Instances.ALL_DAY, - }; - private static final int INSTANCES_INDEX_EVENTID = 0; - private static final int INSTANCES_INDEX_BEGIN = 1; - private static final int INSTANCES_INDEX_ALL_DAY = 2; - - private static final String REMINDERS_WHERE = Reminders.METHOD + "=1 AND " - + Reminders.EVENT_ID + " IN "; - static final String[] REMINDERS_PROJECTION = new String[] { - Reminders.EVENT_ID, - Reminders.MINUTES, - Reminders.METHOD, - }; - private static final int REMINDERS_INDEX_EVENT_ID = 0; - private static final int REMINDERS_INDEX_MINUTES = 1; - private static final int REMINDERS_INDEX_METHOD = 2; - - // Add a slight delay for the EVENT_REMINDER_APP broadcast for a couple reasons: - // (1) so that the concurrent reminder broadcast from the provider doesn't result - // in a double ring, and (2) some OEMs modified the provider to not add an alert to - // the CalendarAlerts table until the alert time, so for the unbundled app's - // notifications to work on these devices, a delay ensures that AlertService won't - // read from the CalendarAlerts table until the alert is present. - static final int ALARM_DELAY_MS = 1000; - - // The reminders query looks like "SELECT ... AND eventId IN 101,102,202,...". This - // sets the max # of events in the query before batching into multiple queries, to - // limit the SQL query length. - private static final int REMINDER_QUERY_BATCH_SIZE = 50; - - // We really need to query for reminder times that fall in some interval, but - // the Reminders table only stores the reminder interval (10min, 15min, etc), and - // we cannot do the join with the Events table to calculate the actual alert time - // from outside of the provider. So the best we can do for now consider events - // whose start times begin within some interval (ie. 1 week out). This means - // reminders which are configured for more than 1 week out won't fire on time. We - // can minimize this to being only 1 day late by putting a 1 day max on the alarm time. - private static final long EVENT_LOOKAHEAD_WINDOW_MS = DateUtils.WEEK_IN_MILLIS; - private static final long MAX_ALARM_ELAPSED_MS = DateUtils.DAY_IN_MILLIS; - - /** - * Schedules the nearest upcoming alarm, to refresh notifications. - * - * This is historically done in the provider but we dupe this here so the unbundled - * app will work on devices that have modified this portion of the provider. This - * has the limitation of querying events within some interval from now (ie. looks at - * reminders for all events occurring in the next week). This means for example, - * a 2 week notification will not fire on time. - */ - public static void scheduleNextAlarm(Context context) { - scheduleNextAlarm(context, AlertUtils.createAlarmManager(context), - REMINDER_QUERY_BATCH_SIZE, System.currentTimeMillis()); - } - - // VisibleForTesting - static void scheduleNextAlarm(Context context, AlarmManagerInterface alarmManager, - int batchSize, long currentMillis) { - Cursor instancesCursor = null; - try { - instancesCursor = queryUpcomingEvents(context, context.getContentResolver(), - currentMillis); - if (instancesCursor != null) { - queryNextReminderAndSchedule(instancesCursor, context, - context.getContentResolver(), alarmManager, batchSize, currentMillis); - } - } finally { - if (instancesCursor != null) { - instancesCursor.close(); - } - } - } - - /** - * Queries events starting within a fixed interval from now. - */ - private static Cursor queryUpcomingEvents(Context context, ContentResolver contentResolver, - long currentMillis) { - Time time = new Time(); - time.normalize(false); - long localOffset = time.gmtoff * 1000; - final long localStartMin = currentMillis; - final long localStartMax = localStartMin + EVENT_LOOKAHEAD_WINDOW_MS; - final long utcStartMin = localStartMin - localOffset; - final long utcStartMax = utcStartMin + EVENT_LOOKAHEAD_WINDOW_MS; - - // Expand Instances table range by a day on either end to account for - // all-day events. - Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon(); - ContentUris.appendId(uriBuilder, localStartMin - DateUtils.DAY_IN_MILLIS); - ContentUris.appendId(uriBuilder, localStartMax + DateUtils.DAY_IN_MILLIS); - - // Build query for all events starting within the fixed interval. - StringBuilder queryBuilder = new StringBuilder(); - queryBuilder.append("("); - queryBuilder.append(INSTANCES_WHERE); - queryBuilder.append(") OR ("); - queryBuilder.append(INSTANCES_WHERE); - queryBuilder.append(")"); - String[] queryArgs = new String[] { - // allday selection - "1", /* visible = ? */ - String.valueOf(utcStartMin), /* begin >= ? */ - String.valueOf(utcStartMax), /* begin <= ? */ - "1", /* allDay = ? */ - - // non-allday selection - "1", /* visible = ? */ - String.valueOf(localStartMin), /* begin >= ? */ - String.valueOf(localStartMax), /* begin <= ? */ - "0" /* allDay = ? */ - }; - - Cursor cursor = contentResolver.query(uriBuilder.build(), INSTANCES_PROJECTION, - queryBuilder.toString(), queryArgs, null); - return cursor; - } - - /** - * Queries for all the reminders of the events in the instancesCursor, and schedules - * the alarm for the next upcoming reminder. - */ - private static void queryNextReminderAndSchedule(Cursor instancesCursor, Context context, - ContentResolver contentResolver, AlarmManagerInterface alarmManager, - int batchSize, long currentMillis) { - if (AlertService.DEBUG) { - int eventCount = instancesCursor.getCount(); - if (eventCount == 0) { - Log.d(TAG, "No events found starting within 1 week."); - } else { - Log.d(TAG, "Query result count for events starting within 1 week: " + eventCount); - } - } - - // Put query results of all events starting within some interval into map of event ID to - // local start time. - Map<Integer, List<Long>> eventMap = new HashMap<Integer, List<Long>>(); - Time timeObj = new Time(); - long nextAlarmTime = Long.MAX_VALUE; - int nextAlarmEventId = 0; - instancesCursor.moveToPosition(-1); - while (!instancesCursor.isAfterLast()) { - int index = 0; - eventMap.clear(); - StringBuilder eventIdsForQuery = new StringBuilder(); - eventIdsForQuery.append('('); - while (index++ < batchSize && instancesCursor.moveToNext()) { - int eventId = instancesCursor.getInt(INSTANCES_INDEX_EVENTID); - long begin = instancesCursor.getLong(INSTANCES_INDEX_BEGIN); - boolean allday = instancesCursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0; - long localStartTime; - if (allday) { - // Adjust allday to local time. - localStartTime = Utils.convertAlldayUtcToLocal(timeObj, begin, - Time.getCurrentTimezone()); - } else { - localStartTime = begin; - } - List<Long> startTimes = eventMap.get(eventId); - if (startTimes == null) { - startTimes = new ArrayList<Long>(); - eventMap.put(eventId, startTimes); - eventIdsForQuery.append(eventId); - eventIdsForQuery.append(","); - } - startTimes.add(localStartTime); - - // Log for debugging. - if (Log.isLoggable(TAG, Log.DEBUG)) { - timeObj.set(localStartTime); - StringBuilder msg = new StringBuilder(); - msg.append("Events cursor result -- eventId:").append(eventId); - msg.append(", allDay:").append(allday); - msg.append(", start:").append(localStartTime); - msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")).append(")"); - Log.d(TAG, msg.toString()); - } - } - if (eventIdsForQuery.charAt(eventIdsForQuery.length() - 1) == ',') { - eventIdsForQuery.deleteCharAt(eventIdsForQuery.length() - 1); - } - eventIdsForQuery.append(')'); - - // Query the reminders table for the events found. - Cursor cursor = null; - try { - cursor = contentResolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION, - REMINDERS_WHERE + eventIdsForQuery, null, null); - - // Process the reminders query results to find the next reminder time. - cursor.moveToPosition(-1); - while (cursor.moveToNext()) { - int eventId = cursor.getInt(REMINDERS_INDEX_EVENT_ID); - int reminderMinutes = cursor.getInt(REMINDERS_INDEX_MINUTES); - List<Long> startTimes = eventMap.get(eventId); - if (startTimes != null) { - for (Long startTime : startTimes) { - long alarmTime = startTime - - reminderMinutes * DateUtils.MINUTE_IN_MILLIS; - if (alarmTime > currentMillis && alarmTime < nextAlarmTime) { - nextAlarmTime = alarmTime; - nextAlarmEventId = eventId; - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - timeObj.set(alarmTime); - StringBuilder msg = new StringBuilder(); - msg.append("Reminders cursor result -- eventId:").append(eventId); - msg.append(", startTime:").append(startTime); - msg.append(", minutes:").append(reminderMinutes); - msg.append(", alarmTime:").append(alarmTime); - msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")) - .append(")"); - Log.d(TAG, msg.toString()); - } - } - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - // Schedule the alarm for the next reminder time. - if (nextAlarmTime < Long.MAX_VALUE) { - scheduleAlarm(context, nextAlarmEventId, nextAlarmTime, currentMillis, alarmManager); - } - } - - /** - * Schedules an alarm for the EVENT_REMINDER_APP broadcast, for the specified - * alarm time with a slight delay (to account for the possible duplicate broadcast - * from the provider). - */ - private static void scheduleAlarm(Context context, long eventId, long alarmTime, - long currentMillis, AlarmManagerInterface alarmManager) { - // Max out the alarm time to 1 day out, so an alert for an event far in the future - // (not present in our event query results for a limited range) can only be at - // most 1 day late. - long maxAlarmTime = currentMillis + MAX_ALARM_ELAPSED_MS; - if (alarmTime > maxAlarmTime) { - alarmTime = maxAlarmTime; - } - - // Add a slight delay (see comments on the member var). - alarmTime += ALARM_DELAY_MS; - - if (AlertService.DEBUG) { - Time time = new Time(); - time.set(alarmTime); - String schedTime = time.format("%a, %b %d, %Y %I:%M%P"); - Log.d(TAG, "Scheduling alarm for EVENT_REMINDER_APP broadcast for event " + eventId - + " at " + alarmTime + " (" + schedTime + ")"); - } - - // Schedule an EVENT_REMINDER_APP broadcast with AlarmManager. The extra is - // only used by AlertService for logging. It is ignored by Intent.filterEquals, - // so this scheduling will still overwrite the alarm that was previously pending. - // Note that the 'setClass' is required, because otherwise it seems the broadcast - // can be eaten by other apps and we somehow may never receive it. - Intent intent = new Intent(AlertReceiver.EVENT_REMINDER_APP_ACTION); - intent.setClass(context, AlertReceiver.class); - intent.putExtra(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime); - PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0); - alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi); - } -} diff --git a/src/com/android/calendar/alerts/AlarmScheduler.kt b/src/com/android/calendar/alerts/AlarmScheduler.kt new file mode 100644 index 00000000..c93bbb04 --- /dev/null +++ b/src/com/android/calendar/alerts/AlarmScheduler.kt @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2021 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.calendar.alerts + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.ContentResolver +import android.content.ContentUris +import android.content.Context +import android.content.Intent +import android.database.Cursor +import android.net.Uri +import android.provider.CalendarContract +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.Instances +import android.provider.CalendarContract.Reminders +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log +import com.android.calendar.Utils +import java.util.HashMap +import java.util.List + +/** + * Schedules the next EVENT_REMINDER_APP broadcast with AlarmManager, by querying the events + * and reminders tables for the next upcoming alert. + */ +object AlarmScheduler { + private const val TAG = "AlarmScheduler" + private val INSTANCES_WHERE: String = (Events.VISIBLE.toString() + "=? AND " + + Instances.BEGIN + ">=? AND " + Instances.BEGIN + "<=? AND " + + Events.ALL_DAY + "=?") + val INSTANCES_PROJECTION = arrayOf<String>( + Instances.EVENT_ID, + Instances.BEGIN, + Instances.ALL_DAY + ) + private const val INSTANCES_INDEX_EVENTID = 0 + private const val INSTANCES_INDEX_BEGIN = 1 + private const val INSTANCES_INDEX_ALL_DAY = 2 + private val REMINDERS_WHERE: String = (Reminders.METHOD.toString() + "=1 AND " + + Reminders.EVENT_ID + " IN ") + val REMINDERS_PROJECTION = arrayOf<String>( + Reminders.EVENT_ID, + Reminders.MINUTES, + Reminders.METHOD + ) + private const val REMINDERS_INDEX_EVENT_ID = 0 + private const val REMINDERS_INDEX_MINUTES = 1 + private const val REMINDERS_INDEX_METHOD = 2 + + // Add a slight delay for the EVENT_REMINDER_APP broadcast for a couple reasons: + // (1) so that the concurrent reminder broadcast from the provider doesn't result + // in a double ring, and (2) some OEMs modified the provider to not add an alert to + // the CalendarAlerts table until the alert time, so for the unbundled app's + // notifications to work on these devices, a delay ensures that AlertService won't + // read from the CalendarAlerts table until the alert is present. + const val ALARM_DELAY_MS = 1000 + + // The reminders query looks like "SELECT ... AND eventId IN 101,102,202,...". This + // sets the max # of events in the query before batching into multiple queries, to + // limit the SQL query length. + private const val REMINDER_QUERY_BATCH_SIZE = 50 + + // We really need to query for reminder times that fall in some interval, but + // the Reminders table only stores the reminder interval (10min, 15min, etc), and + // we cannot do the join with the Events table to calculate the actual alert time + // from outside of the provider. So the best we can do for now consider events + // whose start times begin within some interval (ie. 1 week out). This means + // reminders which are configured for more than 1 week out won't fire on time. We + // can minimize this to being only 1 day late by putting a 1 day max on the alarm time. + private val EVENT_LOOKAHEAD_WINDOW_MS: Long = DateUtils.WEEK_IN_MILLIS + private val MAX_ALARM_ELAPSED_MS: Long = DateUtils.DAY_IN_MILLIS + + /** + * Schedules the nearest upcoming alarm, to refresh notifications. + * + * This is historically done in the provider but we dupe this here so the unbundled + * app will work on devices that have modified this portion of the provider. This + * has the limitation of querying events within some interval from now (ie. looks at + * reminders for all events occurring in the next week). This means for example, + * a 2 week notification will not fire on time. + */ + @JvmStatic fun scheduleNextAlarm(context: Context) { + scheduleNextAlarm( + context, AlertUtils.createAlarmManager(context), + REMINDER_QUERY_BATCH_SIZE, System.currentTimeMillis() + ) + } + + // VisibleForTesting + @JvmStatic fun scheduleNextAlarm( + context: Context, + alarmManager: AlarmManagerInterface?, + batchSize: Int, + currentMillis: Long + ) { + var instancesCursor: Cursor? = null + try { + instancesCursor = queryUpcomingEvents( + context, context.getContentResolver(), + currentMillis + ) + if (instancesCursor != null) { + queryNextReminderAndSchedule( + instancesCursor, + context, + context.getContentResolver(), + alarmManager as AlarmManagerInterface, + batchSize, + currentMillis + ) + } + } finally { + if (instancesCursor != null) { + instancesCursor.close() + } + } + } + + /** + * Queries events starting within a fixed interval from now. + */ + @JvmStatic private fun queryUpcomingEvents( + context: Context, + contentResolver: ContentResolver, + currentMillis: Long + ): Cursor? { + val time = Time() + time.normalize(false) + val localOffset: Long = time.gmtoff * 1000 + val localStartMax = + currentMillis + EVENT_LOOKAHEAD_WINDOW_MS + val utcStartMin = currentMillis - localOffset + val utcStartMax = + utcStartMin + EVENT_LOOKAHEAD_WINDOW_MS + + // Expand Instances table range by a day on either end to account for + // all-day events. + val uriBuilder: Uri.Builder = Instances.CONTENT_URI.buildUpon() + ContentUris.appendId(uriBuilder, currentMillis - DateUtils.DAY_IN_MILLIS) + ContentUris.appendId(uriBuilder, localStartMax + DateUtils.DAY_IN_MILLIS) + + // Build query for all events starting within the fixed interval. + val queryBuilder = StringBuilder() + queryBuilder.append("(") + queryBuilder.append(INSTANCES_WHERE) + queryBuilder.append(") OR (") + queryBuilder.append(INSTANCES_WHERE) + queryBuilder.append(")") + val queryArgs = arrayOf( + // allday selection + "1", /* visible = ? */ + utcStartMin.toString(), /* begin >= ? */ + utcStartMax.toString(), /* begin <= ? */ + "1", /* allDay = ? */ // non-allday selection + "1", /* visible = ? */ + currentMillis.toString(), /* begin >= ? */ + localStartMax.toString(), /* begin <= ? */ + "0" /* allDay = ? */ + ) + + val cursor: Cursor? = contentResolver.query(uriBuilder.build(), INSTANCES_PROJECTION, + queryBuilder.toString(), queryArgs, null) + return cursor + } + + /** + * Queries for all the reminders of the events in the instancesCursor, and schedules + * the alarm for the next upcoming reminder. + */ + @JvmStatic private fun queryNextReminderAndSchedule( + instancesCursor: Cursor, + context: Context, + contentResolver: ContentResolver, + alarmManager: AlarmManagerInterface, + batchSize: Int, + currentMillis: Long + ) { + if (AlertService.DEBUG) { + val eventCount: Int = instancesCursor.getCount() + if (eventCount == 0) { + Log.d(TAG, "No events found starting within 1 week.") + } else { + Log.d(TAG, "Query result count for events starting within 1 week: $eventCount") + } + } + + // Put query results of all events starting within some interval into map of event ID to + // local start time. + val eventMap: HashMap<Int?, List<Long>?> = HashMap<Int?, List<Long>?>() + val timeObj = Time() + var nextAlarmTime = Long.MAX_VALUE + var nextAlarmEventId = 0 + instancesCursor.moveToPosition(-1) + while (!instancesCursor.isAfterLast()) { + var index = 0 + eventMap.clear() + val eventIdsForQuery = StringBuilder() + eventIdsForQuery.append('(') + while (index++ < batchSize && instancesCursor.moveToNext()) { + val eventId: Int = instancesCursor.getInt(INSTANCES_INDEX_EVENTID) + val begin: Long = instancesCursor.getLong(INSTANCES_INDEX_BEGIN) + val allday = instancesCursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0 + var localStartTime: Long + localStartTime = if (allday) { + // Adjust allday to local time. + Utils.convertAlldayUtcToLocal( + timeObj, begin, + Time.getCurrentTimezone() + ) + } else { + begin + } + var startTimes: List<Long>? = eventMap.get(eventId) + if (startTimes == null) { + startTimes = mutableListOf<Long>() as List<Long> + eventMap.put(eventId, startTimes) + eventIdsForQuery.append(eventId) + eventIdsForQuery.append(",") + } + startTimes.add(localStartTime) + + // Log for debugging. + if (Log.isLoggable(TAG, Log.DEBUG)) { + timeObj.set(localStartTime) + val msg = StringBuilder() + msg.append("Events cursor result -- eventId:").append(eventId) + msg.append(", allDay:").append(allday) + msg.append(", start:").append(localStartTime) + msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")).append(")") + Log.d(TAG, msg.toString()) + } + } + if (eventIdsForQuery[eventIdsForQuery.length - 1] == ',') { + eventIdsForQuery.deleteCharAt(eventIdsForQuery.length - 1) + } + eventIdsForQuery.append(')') + + // Query the reminders table for the events found. + var cursor: Cursor? = null + try { + cursor = contentResolver.query( + Reminders.CONTENT_URI, REMINDERS_PROJECTION, + REMINDERS_WHERE + eventIdsForQuery, null, null + ) + + // Process the reminders query results to find the next reminder time. + cursor?.moveToPosition(-1) + while (cursor!!.moveToNext()) { + val eventId: Int = cursor.getInt(REMINDERS_INDEX_EVENT_ID) + val reminderMinutes: Int = cursor.getInt(REMINDERS_INDEX_MINUTES) + val startTimes: List<Long>? = eventMap.get(eventId) + if (startTimes != null) { + for (startTime in startTimes) { + val alarmTime: Long = startTime - + reminderMinutes * DateUtils.MINUTE_IN_MILLIS + if (alarmTime > currentMillis && alarmTime < nextAlarmTime) { + nextAlarmTime = alarmTime + nextAlarmEventId = eventId + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + timeObj.set(alarmTime) + val msg = StringBuilder() + msg.append("Reminders cursor result -- eventId:").append(eventId) + msg.append(", startTime:").append(startTime) + msg.append(", minutes:").append(reminderMinutes) + msg.append(", alarmTime:").append(alarmTime) + msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")) + .append(")") + Log.d(TAG, msg.toString()) + } + } + } + } + } finally { + if (cursor != null) { + cursor.close() + } + } + } + + // Schedule the alarm for the next reminder time. + if (nextAlarmTime < Long.MAX_VALUE) { + scheduleAlarm( + context, + nextAlarmEventId.toLong(), + nextAlarmTime, + currentMillis, + alarmManager + ) + } + } + + /** + * Schedules an alarm for the EVENT_REMINDER_APP broadcast, for the specified + * alarm time with a slight delay (to account for the possible duplicate broadcast + * from the provider). + */ + @JvmStatic private fun scheduleAlarm( + context: Context, + eventId: Long, + alarmTimeInput: Long, + currentMillis: Long, + alarmManager: AlarmManagerInterface + ) { + // Max out the alarm time to 1 day out, so an alert for an event far in the future + // (not present in our event query results for a limited range) can only be at + // most 1 day late. + var alarmTime = alarmTimeInput + val maxAlarmTime = currentMillis + MAX_ALARM_ELAPSED_MS + if (alarmTime > maxAlarmTime) { + alarmTime = maxAlarmTime + } + + // Add a slight delay (see comments on the member var). + alarmTime += ALARM_DELAY_MS.toLong() + if (AlertService.DEBUG) { + val time = Time() + time.set(alarmTime) + val schedTime: String = time.format("%a, %b %d, %Y %I:%M%P") + Log.d( + TAG, "Scheduling alarm for EVENT_REMINDER_APP broadcast for event " + eventId + + " at " + alarmTime + " (" + schedTime + ")" + ) + } + + // Schedule an EVENT_REMINDER_APP broadcast with AlarmManager. The extra is + // only used by AlertService for logging. It is ignored by Intent.filterEquals, + // so this scheduling will still overwrite the alarm that was previously pending. + // Note that the 'setClass' is required, because otherwise it seems the broadcast + // can be eaten by other apps and we somehow may never receive it. + val intent = Intent(AlertReceiver.EVENT_REMINDER_APP_ACTION) + intent.setClass(context, AlertReceiver::class.java) + intent.putExtra(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime) + val pi: PendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0) + alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi) + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/alerts/AlertReceiver.java b/src/com/android/calendar/alerts/AlertReceiver.java deleted file mode 100644 index ce80cae1..00000000 --- a/src/com/android/calendar/alerts/AlertReceiver.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2007 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.calendar.alerts; - -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.database.Cursor; -import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.PowerManager; -import android.provider.CalendarContract.Attendees; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Events; -import android.telephony.TelephonyManager; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.RelativeSizeSpan; -import android.text.style.TextAppearanceSpan; -import android.text.style.URLSpan; -import android.util.Log; -import android.view.View; -import android.widget.RemoteViews; - -import com.android.calendar.R; -import com.android.calendar.Utils; -import com.android.calendar.alerts.AlertService.NotificationWrapper; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Receives android.intent.action.EVENT_REMINDER intents and handles - * event reminders. The intent URI specifies an alert id in the - * CalendarAlerts database table. This class also receives the - * BOOT_COMPLETED intent so that it can add a status bar notification - * if there are Calendar event alarms that have not been dismissed. - * It also receives the TIME_CHANGED action so that it can fire off - * snoozed alarms that have become ready. The real work is done in - * the AlertService class. - * - * To trigger this code after pushing the apk to device: - * adb shell am broadcast -a "android.intent.action.EVENT_REMINDER" - * -n "com.android.calendar/.alerts.AlertReceiver" - */ -public class AlertReceiver extends BroadcastReceiver { - private static final String TAG = "AlertReceiver"; - - // The broadcast for notification refreshes scheduled by the app. This is to - // distinguish the EVENT_REMINDER broadcast sent by the provider. - public static final String EVENT_REMINDER_APP_ACTION = - "com.android.calendar.EVENT_REMINDER_APP"; - - public static final String ACTION_DISMISS_OLD_REMINDERS = "removeOldReminders"; - - @Override - public void onReceive(final Context context, final Intent intent) { - if (AlertService.DEBUG) { - Log.d(TAG, "onReceive: a=" + intent.getAction() + " " + intent.toString()); - } - closeNotificationShade(context); - } - - public static NotificationWrapper makeBasicNotification(Context context, String title, - String summaryText, long startMillis, long endMillis, long eventId, - int notificationId, boolean doPopup, int priority) { - Notification n = buildBasicNotification(new Notification.Builder(context), - context, title, summaryText, startMillis, endMillis, eventId, notificationId, - doPopup, priority, false); - return new NotificationWrapper(n, notificationId, eventId, startMillis, endMillis, doPopup); - } - - private static Notification buildBasicNotification(Notification.Builder notificationBuilder, - Context context, String title, String summaryText, long startMillis, long endMillis, - long eventId, int notificationId, boolean doPopup, int priority, - boolean addActionButtons) { - Resources resources = context.getResources(); - if (title == null || title.length() == 0) { - title = resources.getString(R.string.no_title_label); - } - - // Create the base notification. - notificationBuilder.setContentTitle(title); - notificationBuilder.setContentText(summaryText); - notificationBuilder.setSmallIcon(R.drawable.stat_notify_calendar); - if (Utils.isJellybeanOrLater()) { - // Turn off timestamp. - notificationBuilder.setWhen(0); - - // Should be one of the values in Notification (ie. Notification.PRIORITY_HIGH, etc). - // A higher priority will encourage notification manager to expand it. - notificationBuilder.setPriority(priority); - } - return notificationBuilder.getNotification(); - } - - private void closeNotificationShade(Context context) { - Intent closeNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - context.sendBroadcast(closeNotificationShadeIntent); - } -} diff --git a/src/com/android/calendar/alerts/AlertReceiver.kt b/src/com/android/calendar/alerts/AlertReceiver.kt new file mode 100644 index 00000000..21afa90c --- /dev/null +++ b/src/com/android/calendar/alerts/AlertReceiver.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2021 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.calendar.alerts + +import android.app.Notification +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.util.Log +import com.android.calendar.R +import com.android.calendar.Utils +import com.android.calendar.alerts.AlertService.NotificationWrapper + +/** + * Receives android.intent.action.EVENT_REMINDER intents and handles + * event reminders. The intent URI specifies an alert id in the + * CalendarAlerts database table. This class also receives the + * BOOT_COMPLETED intent so that it can add a status bar notification + * if there are Calendar event alarms that have not been dismissed. + * It also receives the TIME_CHANGED action so that it can fire off + * snoozed alarms that have become ready. The real work is done in + * the AlertService class. + * + * To trigger this code after pushing the apk to device: + * adb shell am broadcast -a "android.intent.action.EVENT_REMINDER" + * -n "com.android.calendar/.alerts.AlertReceiver" + */ +class AlertReceiver : BroadcastReceiver() { + @Override + override fun onReceive(context: Context, intent: Intent) { + if (AlertService.DEBUG) { + Log.d(TAG, "onReceive: a=" + intent.getAction().toString() + " " + intent.toString()) + } + } + + companion object { + private const val TAG = "AlertReceiver" + + // The broadcast for notification refreshes scheduled by the app. This is to + // distinguish the EVENT_REMINDER broadcast sent by the provider. + const val EVENT_REMINDER_APP_ACTION = "com.android.calendar.EVENT_REMINDER_APP" + const val ACTION_DISMISS_OLD_REMINDERS = "removeOldReminders" + fun makeBasicNotification( + context: Context, + title: String, + summaryText: String, + startMillis: Long, + endMillis: Long, + eventId: Long, + notificationId: Int, + doPopup: Boolean, + priority: Int + ): NotificationWrapper { + val n: Notification = buildBasicNotification( + Notification.Builder(context), + context, + title, + summaryText, + startMillis, + endMillis, + eventId, + notificationId, + doPopup, + priority, false + ) + return NotificationWrapper(n, notificationId, eventId, startMillis, endMillis, doPopup) + } + + private fun buildBasicNotification( + notificationBuilder: Notification.Builder, + context: Context, + title: String, + summaryText: String, + startMillis: Long, + endMillis: Long, + eventId: Long, + notificationId: Int, + doPopup: Boolean, + priority: Int, + addActionButtons: Boolean + ): Notification { + var title: String? = title + val resources: Resources = context.getResources() + if (title == null || title.length == 0) { + title = resources.getString(R.string.no_title_label) + } + + // Create the base notification. + notificationBuilder.setContentTitle(title) + notificationBuilder.setContentText(summaryText) + notificationBuilder.setSmallIcon(R.drawable.stat_notify_calendar) + if (Utils.isJellybeanOrLater()) { + // Turn off timestamp. + notificationBuilder.setWhen(0) + + // Should be one of the values in Notification + // (ie. Notification.PRIORITY_HIGH, etc). + // A higher priority will encourage notification manager to expand it. + notificationBuilder.setPriority(priority) + } + return notificationBuilder.getNotification() + } + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/alerts/AlertService.java b/src/com/android/calendar/alerts/AlertService.java deleted file mode 100644 index d2c994da..00000000 --- a/src/com/android/calendar/alerts/AlertService.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.calendar.alerts; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.Service; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.Process; -import android.provider.CalendarContract; -import android.provider.CalendarContract.Attendees; -import android.provider.CalendarContract.CalendarAlerts; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; - -import com.android.calendar.R; -import com.android.calendar.Utils; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.TimeZone; - -/** - * This service is used to handle calendar event reminders. - */ -public class AlertService extends Service { - static final boolean DEBUG = true; - private static final String TAG = "AlertService"; - - private volatile Looper mServiceLooper; - - static final String[] ALERT_PROJECTION = new String[] { - CalendarAlerts._ID, // 0 - CalendarAlerts.EVENT_ID, // 1 - CalendarAlerts.STATE, // 2 - CalendarAlerts.TITLE, // 3 - CalendarAlerts.EVENT_LOCATION, // 4 - CalendarAlerts.SELF_ATTENDEE_STATUS, // 5 - CalendarAlerts.ALL_DAY, // 6 - CalendarAlerts.ALARM_TIME, // 7 - CalendarAlerts.MINUTES, // 8 - CalendarAlerts.BEGIN, // 9 - CalendarAlerts.END, // 10 - CalendarAlerts.DESCRIPTION, // 11 - }; - - private static final int ALERT_INDEX_ID = 0; - private static final int ALERT_INDEX_EVENT_ID = 1; - private static final int ALERT_INDEX_STATE = 2; - private static final int ALERT_INDEX_TITLE = 3; - private static final int ALERT_INDEX_EVENT_LOCATION = 4; - private static final int ALERT_INDEX_SELF_ATTENDEE_STATUS = 5; - private static final int ALERT_INDEX_ALL_DAY = 6; - private static final int ALERT_INDEX_ALARM_TIME = 7; - private static final int ALERT_INDEX_MINUTES = 8; - private static final int ALERT_INDEX_BEGIN = 9; - private static final int ALERT_INDEX_END = 10; - private static final int ALERT_INDEX_DESCRIPTION = 11; - - private static final String ACTIVE_ALERTS_SELECTION = "(" + CalendarAlerts.STATE + "=? OR " - + CalendarAlerts.STATE + "=?) AND " + CalendarAlerts.ALARM_TIME + "<="; - - private static final String[] ACTIVE_ALERTS_SELECTION_ARGS = new String[] { - Integer.toString(CalendarAlerts.STATE_FIRED), - Integer.toString(CalendarAlerts.STATE_SCHEDULED) - }; - - private static final String ACTIVE_ALERTS_SORT = "begin DESC, end DESC"; - - private static final String DISMISS_OLD_SELECTION = CalendarAlerts.END + "<? AND " - + CalendarAlerts.STATE + "=?"; - - private static final int MINUTE_MS = 60 * 1000; - - // The grace period before changing a notification's priority bucket. - private static final int MIN_DEPRIORITIZE_GRACE_PERIOD_MS = 15 * MINUTE_MS; - - // Hard limit to the number of notifications displayed. - public static final int MAX_NOTIFICATIONS = 20; - - // Added wrapper for testing - public static class NotificationWrapper { - Notification mNotification; - long mEventId; - long mBegin; - long mEnd; - ArrayList<NotificationWrapper> mNw; - - public NotificationWrapper(Notification n, int notificationId, long eventId, - long startMillis, long endMillis, boolean doPopup) { - mNotification = n; - mEventId = eventId; - mBegin = startMillis; - mEnd = endMillis; - - // popup? - // notification id? - } - - public NotificationWrapper(Notification n) { - mNotification = n; - } - - public void add(NotificationWrapper nw) { - if (mNw == null) { - mNw = new ArrayList<NotificationWrapper>(); - } - mNw.add(nw); - } - } - - // Added wrapper for testing - public static class NotificationMgrWrapper extends NotificationMgr { - NotificationManager mNm; - - public NotificationMgrWrapper(NotificationManager nm) { - mNm = nm; - } - - @Override - public void cancel(int id) { - mNm.cancel(id); - } - - @Override - public void notify(int id, NotificationWrapper nw) { - mNm.notify(id, nw.mNotification); - } - } - - static class NotificationInfo { - String eventName; - String location; - String description; - long startMillis; - long endMillis; - long eventId; - boolean allDay; - boolean newAlert; - - NotificationInfo(String eventName, String location, String description, long startMillis, - long endMillis, long eventId, boolean allDay, boolean newAlert) { - this.eventName = eventName; - this.location = location; - this.description = description; - this.startMillis = startMillis; - this.endMillis = endMillis; - this.eventId = eventId; - this.newAlert = newAlert; - this.allDay = allDay; - } - } - - @Override - public void onCreate() { - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return START_REDELIVER_INTENT; - } - - @Override - public void onDestroy() { - mServiceLooper.quit(); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } -} diff --git a/src/com/android/calendar/alerts/AlertService.kt b/src/com/android/calendar/alerts/AlertService.kt new file mode 100644 index 00000000..bc1b4e04 --- /dev/null +++ b/src/com/android/calendar/alerts/AlertService.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2021 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.calendar.alerts + +import android.app.Notification +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.os.Looper +import android.provider.CalendarContract.CalendarAlerts +import java.util.ArrayList + +/** + * This service is used to handle calendar event reminders. + */ +class AlertService : Service() { + @Volatile + private var mServiceLooper: Looper? = null + + // Added wrapper for testing + class NotificationWrapper { + var mNotification: Notification + var mEventId: Long = 0 + var mBegin: Long = 0 + var mEnd: Long = 0 + var mNw: ArrayList<NotificationWrapper>? = null + + constructor( + n: Notification, + notificationId: Int, + eventId: Long, + startMillis: Long, + endMillis: Long, + doPopup: Boolean + ) { + mNotification = n + mEventId = eventId + mBegin = startMillis + mEnd = endMillis + + // popup? + // notification id? + } + + constructor(n: Notification) { + mNotification = n + } + + fun add(nw: NotificationWrapper?) { + val temp = mNw + if (temp == null) { + mNw = ArrayList<NotificationWrapper>() + } + mNw?.add(nw as AlertService.NotificationWrapper) + } + } + + // Added wrapper for testing + class NotificationMgrWrapper(nm: NotificationManager) : NotificationMgr() { + var mNm: NotificationManager + @Override + override fun cancel(id: Int) { + mNm.cancel(id) + } + + @Override + override fun notify(id: Int, nw: NotificationWrapper?) { + mNm.notify(id, nw?.mNotification) + } + + init { + mNm = nm + } + } + + internal class NotificationInfo( + var eventName: String, + var location: String, + var description: String, + var startMillis: Long, + var endMillis: Long, + var eventId: Long, + var allDay: Boolean, + var newAlert: Boolean + ) + + @Override + override fun onCreate() { + } + + @Override + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return START_REDELIVER_INTENT + } + + @Override + override fun onDestroy() { + mServiceLooper?.quit() + } + + @Override + override fun onBind(intent: Intent?): IBinder? { + return null + } + + companion object { + const val DEBUG = true + private const val TAG = "AlertService" + val ALERT_PROJECTION = arrayOf<String>( + CalendarAlerts._ID, // 0 + CalendarAlerts.EVENT_ID, // 1 + CalendarAlerts.STATE, // 2 + CalendarAlerts.TITLE, // 3 + CalendarAlerts.EVENT_LOCATION, // 4 + CalendarAlerts.SELF_ATTENDEE_STATUS, // 5 + CalendarAlerts.ALL_DAY, // 6 + CalendarAlerts.ALARM_TIME, // 7 + CalendarAlerts.MINUTES, // 8 + CalendarAlerts.BEGIN, // 9 + CalendarAlerts.END, // 10 + CalendarAlerts.DESCRIPTION + ) + private const val ALERT_INDEX_ID = 0 + private const val ALERT_INDEX_EVENT_ID = 1 + private const val ALERT_INDEX_STATE = 2 + private const val ALERT_INDEX_TITLE = 3 + private const val ALERT_INDEX_EVENT_LOCATION = 4 + private const val ALERT_INDEX_SELF_ATTENDEE_STATUS = 5 + private const val ALERT_INDEX_ALL_DAY = 6 + private const val ALERT_INDEX_ALARM_TIME = 7 + private const val ALERT_INDEX_MINUTES = 8 + private const val ALERT_INDEX_BEGIN = 9 + private const val ALERT_INDEX_END = 10 + private const val ALERT_INDEX_DESCRIPTION = 11 + private val ACTIVE_ALERTS_SELECTION = ("(" + CalendarAlerts.STATE.toString() + "=? OR " + + CalendarAlerts.STATE.toString() + "=?) AND " + + CalendarAlerts.ALARM_TIME.toString() + "<=") + private val ACTIVE_ALERTS_SELECTION_ARGS = arrayOf<String>( + Integer.toString(CalendarAlerts.STATE_FIRED), + Integer.toString(CalendarAlerts.STATE_SCHEDULED) + ) + private const val ACTIVE_ALERTS_SORT = "begin DESC, end DESC" + private val DISMISS_OLD_SELECTION: String = (CalendarAlerts.END.toString() + "<? AND " + + CalendarAlerts.STATE + "=?") + private const val MINUTE_MS = 60 * 1000 + + // The grace period before changing a notification's priority bucket. + private const val MIN_DEPRIORITIZE_GRACE_PERIOD_MS = 15 * MINUTE_MS + + // Hard limit to the number of notifications displayed. + const val MAX_NOTIFICATIONS = 20 + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/alerts/AlertUtils.java b/src/com/android/calendar/alerts/AlertUtils.java deleted file mode 100644 index b9aaec29..00000000 --- a/src/com/android/calendar/alerts/AlertUtils.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.calendar.alerts; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.provider.CalendarContract; -import android.provider.CalendarContract.CalendarAlerts; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; - -import com.android.calendar.EventInfoActivity; -import com.android.calendar.R; -import com.android.calendar.Utils; - -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; - -public class AlertUtils { - private static final String TAG = "AlertUtils"; - static final boolean DEBUG = true; - - public static final long SNOOZE_DELAY = 5 * 60 * 1000L; - - // We use one notification id for the expired events notification. All - // other notifications (the 'active' future/concurrent ones) use a unique ID. - public static final int EXPIRED_GROUP_NOTIFICATION_ID = 0; - - public static final String EVENT_ID_KEY = "eventid"; - public static final String EVENT_START_KEY = "eventstart"; - public static final String EVENT_END_KEY = "eventend"; - public static final String NOTIFICATION_ID_KEY = "notificationid"; - public static final String EVENT_IDS_KEY = "eventids"; - public static final String EVENT_STARTS_KEY = "starts"; - - // A flag for using local storage to save alert state instead of the alerts DB table. - // This allows the unbundled app to run alongside other calendar apps without eating - // alerts from other apps. - static boolean BYPASS_DB = true; - - /** - * Creates an AlarmManagerInterface that wraps a real AlarmManager. The alarm code - * was abstracted to an interface to make it testable. - */ - public static AlarmManagerInterface createAlarmManager(Context context) { - final AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - return new AlarmManagerInterface() { - @Override - public void set(int type, long triggerAtMillis, PendingIntent operation) { - if (Utils.isKeyLimePieOrLater()) { - mgr.setExact(type, triggerAtMillis, operation); - } else { - mgr.set(type, triggerAtMillis, operation); - } - } - }; - } - - /** - * Schedules an alarm intent with the system AlarmManager that will notify - * listeners when a reminder should be fired. The provider will keep - * scheduled reminders up to date but apps may use this to implement snooze - * functionality without modifying the reminders table. Scheduled alarms - * will generate an intent using AlertReceiver.EVENT_REMINDER_APP_ACTION. - * - * @param context A context for referencing system resources - * @param manager The AlarmManager to use or null - * @param alarmTime The time to fire the intent in UTC millis since epoch - */ - public static void scheduleAlarm(Context context, AlarmManagerInterface manager, - long alarmTime) { - } - - public static Intent buildEventViewIntent(Context c, long eventId, long begin, long end) { - Intent i = new Intent(Intent.ACTION_VIEW); - Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); - builder.appendEncodedPath("events/" + eventId); - i.setData(builder.build()); - i.setClass(c, EventInfoActivity.class); - i.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, begin); - i.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, end); - return i; - } -} diff --git a/src/com/android/calendar/alerts/AlertUtils.kt b/src/com/android/calendar/alerts/AlertUtils.kt new file mode 100644 index 00000000..18b7e7d1 --- /dev/null +++ b/src/com/android/calendar/alerts/AlertUtils.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2021 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.calendar.alerts + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.CalendarContract +import com.android.calendar.EventInfoActivity +import com.android.calendar.Utils + +object AlertUtils { + private const val TAG = "AlertUtils" + const val DEBUG = true + const val SNOOZE_DELAY = 5 * 60 * 1000L + + // We use one notification id for the expired events notification. All + // other notifications (the 'active' future/concurrent ones) use a unique ID. + const val EXPIRED_GROUP_NOTIFICATION_ID = 0 + const val EVENT_ID_KEY = "eventid" + const val EVENT_START_KEY = "eventstart" + const val EVENT_END_KEY = "eventend" + const val NOTIFICATION_ID_KEY = "notificationid" + const val EVENT_IDS_KEY = "eventids" + const val EVENT_STARTS_KEY = "starts" + + // A flag for using local storage to save alert state instead of the alerts DB table. + // This allows the unbundled app to run alongside other calendar apps without eating + // alerts from other apps. + var BYPASS_DB = true + + /** + * Creates an AlarmManagerInterface that wraps a real AlarmManager. The alarm code + * was abstracted to an interface to make it testable. + */ + @JvmStatic fun createAlarmManager(context: Context): AlarmManagerInterface { + val mgr: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + return object : AlarmManagerInterface { + override operator fun set(type: Int, triggerAtMillis: Long, operation: PendingIntent?) { + if (com.android.calendar.Utils.isKeyLimePieOrLater()) { + mgr.setExact(type, triggerAtMillis, operation) + } else { + mgr.set(type, triggerAtMillis, operation) + } + } + } + } + + /** + * Schedules an alarm intent with the system AlarmManager that will notify + * listeners when a reminder should be fired. The provider will keep + * scheduled reminders up to date but apps may use this to implement snooze + * functionality without modifying the reminders table. Scheduled alarms + * will generate an intent using AlertReceiver.EVENT_REMINDER_APP_ACTION. + * + * @param context A context for referencing system resources + * @param manager The AlarmManager to use or null + * @param alarmTime The time to fire the intent in UTC millis since epoch + */ + @JvmStatic fun scheduleAlarm( + context: Context?, + manager: AlarmManagerInterface?, + alarmTime: Long + ) { + } + + @JvmStatic fun buildEventViewIntent(c: Context, eventId: Long, begin: Long, end: Long): Intent { + val i = Intent(Intent.ACTION_VIEW) + val builder: Uri.Builder = CalendarContract.CONTENT_URI.buildUpon() + builder.appendEncodedPath("events/$eventId") + i.setData(builder.build()) + i.setClass(c, EventInfoActivity::class.java) + i.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, begin) + i.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, end) + return i + } +} diff --git a/src/com/android/calendar/alerts/DismissAlarmsService.java b/src/com/android/calendar/alerts/DismissAlarmsService.java deleted file mode 100644 index 1ec3c22d..00000000 --- a/src/com/android/calendar/alerts/DismissAlarmsService.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2009 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.calendar.alerts; - -import android.app.IntentService; -import android.app.NotificationManager; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.IBinder; -import android.provider.CalendarContract.CalendarAlerts; -import androidx.core.app.TaskStackBuilder; - -import android.util.Log; -import com.android.calendar.EventInfoActivity; -import com.android.calendar.alerts.GlobalDismissManager.AlarmId; - -import java.util.LinkedList; -import java.util.List; - -/** - * Service for asynchronously marking fired alarms as dismissed. - */ -public class DismissAlarmsService extends IntentService { - private static final String TAG = "DismissAlarmsService"; - public static final String SHOW_ACTION = "com.android.calendar.SHOW"; - public static final String DISMISS_ACTION = "com.android.calendar.DISMISS"; - - private static final String[] PROJECTION = new String[] { - CalendarAlerts.STATE, - }; - private static final int COLUMN_INDEX_STATE = 0; - - public DismissAlarmsService() { - super("DismissAlarmsService"); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onHandleIntent(Intent intent) { - if (AlertService.DEBUG) { - Log.d(TAG, "onReceive: a=" + intent.getAction() + " " + intent.toString()); - } - - long eventId = intent.getLongExtra(AlertUtils.EVENT_ID_KEY, -1); - long eventStart = intent.getLongExtra(AlertUtils.EVENT_START_KEY, -1); - long eventEnd = intent.getLongExtra(AlertUtils.EVENT_END_KEY, -1); - long[] eventIds = intent.getLongArrayExtra(AlertUtils.EVENT_IDS_KEY); - long[] eventStarts = intent.getLongArrayExtra(AlertUtils.EVENT_STARTS_KEY); - int notificationId = intent.getIntExtra(AlertUtils.NOTIFICATION_ID_KEY, -1); - List<AlarmId> alarmIds = new LinkedList<AlarmId>(); - - Uri uri = CalendarAlerts.CONTENT_URI; - String selection; - - // Dismiss a specific fired alarm if id is present, otherwise, dismiss all alarms - if (eventId != -1) { - alarmIds.add(new AlarmId(eventId, eventStart)); - selection = CalendarAlerts.STATE + "=" + CalendarAlerts.STATE_FIRED + " AND " + - CalendarAlerts.EVENT_ID + "=" + eventId; - } else if (eventIds != null && eventIds.length > 0 && - eventStarts != null && eventIds.length == eventStarts.length) { - selection = buildMultipleEventsQuery(eventIds); - for (int i = 0; i < eventIds.length; i++) { - alarmIds.add(new AlarmId(eventIds[i], eventStarts[i])); - } - } else { - // NOTE: I don't believe that this ever happens. - selection = CalendarAlerts.STATE + "=" + CalendarAlerts.STATE_FIRED; - } - - GlobalDismissManager.dismissGlobally(getApplicationContext(), alarmIds); - - ContentResolver resolver = getContentResolver(); - ContentValues values = new ContentValues(); - values.put(PROJECTION[COLUMN_INDEX_STATE], CalendarAlerts.STATE_DISMISSED); - resolver.update(uri, values, selection, null); - - // Remove from notification bar. - if (notificationId != -1) { - NotificationManager nm = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.cancel(notificationId); - } - - if (SHOW_ACTION.equals(intent.getAction())) { - // Show event on Calendar app by building an intent and task stack to start - // EventInfoActivity with AllInOneActivity as the parent activity rooted to home. - Intent i = AlertUtils.buildEventViewIntent(this, eventId, eventStart, eventEnd); - - TaskStackBuilder.create(this) - .addParentStack(EventInfoActivity.class).addNextIntent(i).startActivities(); - } - } - - private String buildMultipleEventsQuery(long[] eventIds) { - StringBuilder selection = new StringBuilder(); - selection.append(CalendarAlerts.STATE); - selection.append("="); - selection.append(CalendarAlerts.STATE_FIRED); - if (eventIds.length > 0) { - selection.append(" AND ("); - selection.append(CalendarAlerts.EVENT_ID); - selection.append("="); - selection.append(eventIds[0]); - for (int i = 1; i < eventIds.length; i++) { - selection.append(" OR "); - selection.append(CalendarAlerts.EVENT_ID); - selection.append("="); - selection.append(eventIds[i]); - } - selection.append(")"); - } - return selection.toString(); - } -} diff --git a/src/com/android/calendar/alerts/DismissAlarmsService.kt b/src/com/android/calendar/alerts/DismissAlarmsService.kt new file mode 100644 index 00000000..88683d3a --- /dev/null +++ b/src/com/android/calendar/alerts/DismissAlarmsService.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2021 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.calendar.alerts + +import android.app.IntentService +import android.app.NotificationManager +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.IBinder +import android.provider.CalendarContract.CalendarAlerts +import androidx.core.app.TaskStackBuilder +import android.util.Log +import com.android.calendar.EventInfoActivity +import com.android.calendar.alerts.GlobalDismissManager.AlarmId +import java.util.LinkedList +import java.util.List + +/** + * Service for asynchronously marking fired alarms as dismissed. + */ +class DismissAlarmsService : IntentService("DismissAlarmsService") { + @Override + override fun onBind(intent: Intent?): IBinder? { + return null + } + + @Override + override fun onHandleIntent(intent: Intent?) { + if (AlertService.DEBUG) { + Log.d(TAG, "onReceive: a=" + intent?.getAction().toString() + " " + intent.toString()) + } + val eventId = intent?.getLongExtra(AlertUtils.EVENT_ID_KEY, -1) + val eventStart = intent?.getLongExtra(AlertUtils.EVENT_START_KEY, -1) + val eventEnd = intent?.getLongExtra(AlertUtils.EVENT_END_KEY, -1) + val eventIds = intent?.getLongArrayExtra(AlertUtils.EVENT_IDS_KEY) + val eventStarts = intent?.getLongArrayExtra(AlertUtils.EVENT_STARTS_KEY) + val notificationId = intent?.getIntExtra(AlertUtils.NOTIFICATION_ID_KEY, -1) + val alarmIds = LinkedList<AlarmId>() + val uri: Uri = CalendarAlerts.CONTENT_URI + val selection: String + + // Dismiss a specific fired alarm if id is present, otherwise, dismiss all alarms + if (eventId != -1L) { + alarmIds.add(AlarmId(eventId as Long, eventStart as Long)) + selection = + CalendarAlerts.STATE.toString() + "=" + CalendarAlerts.STATE_FIRED + " AND " + + CalendarAlerts.EVENT_ID + "=" + eventId + } else if (eventIds != null && eventIds.size > 0 && eventStarts != null && + eventIds.size == eventStarts.size) { + selection = buildMultipleEventsQuery(eventIds) + for (i in eventIds.indices) { + alarmIds.add(AlarmId(eventIds[i], eventStarts[i])) + } + } else { + // NOTE: I don't believe that this ever happens. + selection = CalendarAlerts.STATE.toString() + "=" + CalendarAlerts.STATE_FIRED + } + GlobalDismissManager.dismissGlobally(getApplicationContext(), + alarmIds as List<GlobalDismissManager.AlarmId>) + val resolver: ContentResolver = getContentResolver() + val values = ContentValues() + values.put(PROJECTION[COLUMN_INDEX_STATE], CalendarAlerts.STATE_DISMISSED) + resolver.update(uri, values, selection, null) + + // Remove from notification bar. + if (notificationId != -1) { + val nm: NotificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + nm.cancel(notificationId as Int) + } + if (SHOW_ACTION.equals(intent?.getAction())) { + // Show event on Calendar app by building an intent and task stack to start + // EventInfoActivity with AllInOneActivity as the parent activity rooted to home. + val i: Intent = AlertUtils.buildEventViewIntent(this, eventId as Long, + eventStart as Long, eventEnd as Long) + TaskStackBuilder.create(this) + .addParentStack(EventInfoActivity::class.java).addNextIntent(i).startActivities() + } + } + + private fun buildMultipleEventsQuery(eventIds: LongArray): String { + val selection = StringBuilder() + selection.append(CalendarAlerts.STATE) + selection.append("=") + selection.append(CalendarAlerts.STATE_FIRED) + if (eventIds.size > 0) { + selection.append(" AND (") + selection.append(CalendarAlerts.EVENT_ID) + selection.append("=") + selection.append(eventIds[0]) + for (i in 1 until eventIds.size) { + selection.append(" OR ") + selection.append(CalendarAlerts.EVENT_ID) + selection.append("=") + selection.append(eventIds[i]) + } + selection.append(")") + } + return selection.toString() + } + + companion object { + private const val TAG = "DismissAlarmsService" + const val SHOW_ACTION = "com.android.calendar.SHOW" + const val DISMISS_ACTION = "com.android.calendar.DISMISS" + private val PROJECTION = arrayOf<String>( + CalendarAlerts.STATE + ) + private const val COLUMN_INDEX_STATE = 0 + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/alerts/GlobalDismissManager.java b/src/com/android/calendar/alerts/GlobalDismissManager.java deleted file mode 100644 index 27b3e162..00000000 --- a/src/com/android/calendar/alerts/GlobalDismissManager.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 com.android.calendar.alerts; - -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.provider.CalendarContract.CalendarAlerts; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Events; -import android.util.Log; -import android.util.Pair; - -import com.android.calendar.R; - -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Utilities for managing notification dismissal across devices. - */ -public class GlobalDismissManager extends BroadcastReceiver { - public static class AlarmId { - public long mEventId; - public long mStart; - - public AlarmId(long id, long start) { - mEventId = id; - mStart = start; - } - } - - /** - * Globally dismiss notifications that are backed by the same events. - * - * @param context application context - * @param alarmIds Unique identifiers for events that have been dismissed by the user. - * @return true if notification_sender_id is available - */ - public static void dismissGlobally(Context context, List<AlarmId> alarmIds) { - Set<Long> eventIds = new HashSet<Long>(alarmIds.size()); - for (AlarmId alarmId: alarmIds) { - eventIds.add(alarmId.mEventId); - } - } - - @Override - @SuppressWarnings("unchecked") - public void onReceive(Context context, Intent intent) { - new AsyncTask<Pair<Context, Intent>, Void, Void>() { - @Override - protected Void doInBackground(Pair<Context, Intent>... params) { - return null; - } - }.execute(new Pair<Context, Intent>(context, intent)); - } -} diff --git a/src/com/android/calendar/alerts/GlobalDismissManager.kt b/src/com/android/calendar/alerts/GlobalDismissManager.kt new file mode 100644 index 00000000..4cf0bc0c --- /dev/null +++ b/src/com/android/calendar/alerts/GlobalDismissManager.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 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.calendar.alerts + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.AsyncTask +import android.util.Pair +import java.util.HashSet +import java.util.List + +/** + * Utilities for managing notification dismissal across devices. + */ +class GlobalDismissManager : BroadcastReceiver() { + class AlarmId(var mEventId: Long, var mStart: Long) + + @Override + @SuppressWarnings("unchecked") + override fun onReceive(context: Context?, intent: Intent?) { + object : AsyncTask<Pair<Context?, Intent?>?, Void?, Void?>() { + @Override + protected override fun doInBackground(vararg params: Pair<Context?, Intent?>?): Void? { + return null + } + }.execute(Pair<Context?, Intent?>(context, intent)) + } + + companion object { + /** + * Globally dismiss notifications that are backed by the same events. + * + * @param context application context + * @param alarmIds Unique identifiers for events that have been dismissed by the user. + * @return true if notification_sender_id is available + */ + @JvmStatic fun dismissGlobally(context: Context?, alarmIds: List<AlarmId>) { + val eventIds: HashSet<Long> = HashSet<Long>(alarmIds.size) + for (alarmId in alarmIds) { + eventIds.add(alarmId.mEventId) + } + } + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/alerts/InitAlarmsService.java b/src/com/android/calendar/alerts/InitAlarmsService.java deleted file mode 100644 index 3a9b0b2c..00000000 --- a/src/com/android/calendar/alerts/InitAlarmsService.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.calendar.alerts; - -import android.app.IntentService; -import android.content.ContentValues; -import android.content.Intent; -import android.net.Uri; -import android.os.SystemClock; -import android.provider.CalendarContract; -import android.util.Log; - -/** - * Service for clearing all scheduled alerts from the CalendarAlerts table and - * rescheduling them. This is expected to be called only on boot up, to restore - * the AlarmManager alarms that were lost on device restart. - */ -public class InitAlarmsService extends IntentService { - private static final String TAG = "InitAlarmsService"; - private static final String SCHEDULE_ALARM_REMOVE_PATH = "schedule_alarms_remove"; - private static final Uri SCHEDULE_ALARM_REMOVE_URI = Uri.withAppendedPath( - CalendarContract.CONTENT_URI, SCHEDULE_ALARM_REMOVE_PATH); - - // Delay for rescheduling the alarms must be great enough to minimize race - // conditions with the provider's boot up actions. - private static final long DELAY_MS = 30000; - - public InitAlarmsService() { - super("InitAlarmsService"); - } - - @Override - protected void onHandleIntent(Intent intent) { - // Delay to avoid race condition of in-progress alarm scheduling in provider. - SystemClock.sleep(DELAY_MS); - Log.d(TAG, "Clearing and rescheduling alarms."); - try { - getContentResolver().update(SCHEDULE_ALARM_REMOVE_URI, new ContentValues(), null, - null); - } catch (java.lang.IllegalArgumentException e) { - // java.lang.IllegalArgumentException: - // Unknown URI content://com.android.calendar/schedule_alarms_remove - - // Until b/7742576 is resolved, just catch the exception so the app won't crash - Log.e(TAG, "update failed: " + e.toString()); - } - } -} diff --git a/src/com/android/calendar/alerts/InitAlarmsService.kt b/src/com/android/calendar/alerts/InitAlarmsService.kt new file mode 100644 index 00000000..0ac8a474 --- /dev/null +++ b/src/com/android/calendar/alerts/InitAlarmsService.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 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.calendar.alerts + +import android.app.IntentService +import android.content.ContentValues +import android.content.Intent +import android.net.Uri +import android.os.SystemClock +import android.provider.CalendarContract +import android.util.Log + +/** + * Service for clearing all scheduled alerts from the CalendarAlerts table and + * rescheduling them. This is expected to be called only on boot up, to restore + * the AlarmManager alarms that were lost on device restart. + */ +class InitAlarmsService : IntentService("InitAlarmsService") { + @Override + protected override fun onHandleIntent(intent: Intent?) { + // Delay to avoid race condition of in-progress alarm scheduling in provider. + SystemClock.sleep(DELAY_MS) + Log.d(TAG, "Clearing and rescheduling alarms.") + try { + getContentResolver().update( + SCHEDULE_ALARM_REMOVE_URI, ContentValues(), null, + null + ) + } catch (e: java.lang.IllegalArgumentException) { + // java.lang.IllegalArgumentException: + // Unknown URI content://com.android.calendar/schedule_alarms_remove + + // Until b/7742576 is resolved, just catch the exception so the app won't crash + Log.e(TAG, "update failed: " + e.toString()) + } + } + + companion object { + private const val TAG = "InitAlarmsService" + private const val SCHEDULE_ALARM_REMOVE_PATH = "schedule_alarms_remove" + private val SCHEDULE_ALARM_REMOVE_URI: Uri = Uri.withAppendedPath( + CalendarContract.CONTENT_URI, SCHEDULE_ALARM_REMOVE_PATH + ) + + // Delay for rescheduling the alarms must be great enough to minimize race + // conditions with the provider's boot up actions. + private const val DELAY_MS: Long = 30000 + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/alerts/NotificationMgr.java b/src/com/android/calendar/alerts/NotificationMgr.kt index 0ab475c3..609b8141 100644 --- a/src/com/android/calendar/alerts/NotificationMgr.java +++ b/src/com/android/calendar/alerts/NotificationMgr.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * Copyright (C) 2021 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. @@ -13,29 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.calendar.alerts -package com.android.calendar.alerts; +import com.android.calendar.alerts.AlertService.NotificationWrapper -import com.android.calendar.alerts.AlertService.NotificationWrapper; - -public abstract class NotificationMgr { - public abstract void notify(int id, NotificationWrapper notification); - public abstract void cancel(int id); +abstract class NotificationMgr { + abstract fun notify(id: Int, notification: NotificationWrapper?) + abstract fun cancel(id: Int) /** * Don't actually use the notification framework's cancelAll since the SyncAdapter * might post notifications and we don't want to affect those. */ - public void cancelAll() { - cancelAllBetween(0, AlertService.MAX_NOTIFICATIONS); + fun cancelAll() { + cancelAllBetween(0, AlertService.MAX_NOTIFICATIONS) } /** * Cancels IDs between the specified bounds, inclusively. */ - public void cancelAllBetween(int from, int to) { - for (int i = from; i <= to; i++) { - cancel(i); + fun cancelAllBetween(from: Int, to: Int) { + for (i in from..to) { + cancel(i) } } -} +}
\ No newline at end of file diff --git a/src/com/android/calendar/alerts/QuickResponseActivity.java b/src/com/android/calendar/alerts/QuickResponseActivity.java deleted file mode 100644 index 3d291d02..00000000 --- a/src/com/android/calendar/alerts/QuickResponseActivity.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.calendar.alerts; - -import android.app.ListActivity; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; -import android.widget.Toast; - -import com.android.calendar.R; -import com.android.calendar.Utils; - -import java.util.Arrays; - -/** - * Activity which displays when the user wants to email guests from notifications. - * - * This presents the user with list if quick responses to be populated in an email - * to minimize typing. - * - */ -public class QuickResponseActivity extends ListActivity implements OnItemClickListener { - private static final String TAG = "QuickResponseActivity"; - public static final String EXTRA_EVENT_ID = "eventId"; - - private String[] mResponses = null; - static long mEventId; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - Intent intent = getIntent(); - if (intent == null) { - finish(); - return; - } - - mEventId = intent.getLongExtra(EXTRA_EVENT_ID, -1); - if (mEventId == -1) { - finish(); - return; - } - - // Set listener - getListView().setOnItemClickListener(QuickResponseActivity.this); - - // Populate responses - String[] responses = Utils.getQuickResponses(this); - Arrays.sort(responses); - - // Add "Custom response..." - mResponses = new String[responses.length + 1]; - int i; - for (i = 0; i < responses.length; i++) { - mResponses[i] = responses[i]; - } - mResponses[i] = getResources().getString(R.string.quick_response_custom_msg); - - setListAdapter(new ArrayAdapter<String>(this, R.layout.quick_response_item, mResponses)); - } - - // implements OnItemClickListener - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - - String body = null; - if (mResponses != null && position < mResponses.length - 1) { - body = mResponses[position]; - } - - // Start thread to query provider and send mail - new QueryThread(mEventId, body).start(); - } - - private class QueryThread extends Thread { - long mEventId; - String mBody; - - QueryThread(long eventId, String body) { - mEventId = eventId; - mBody = body; - } - - @Override - public void run() { - } - } -} diff --git a/src/com/android/calendar/alerts/QuickResponseActivity.kt b/src/com/android/calendar/alerts/QuickResponseActivity.kt new file mode 100644 index 00000000..afccaffd --- /dev/null +++ b/src/com/android/calendar/alerts/QuickResponseActivity.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 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.calendar.alerts + +import android.app.ListActivity +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.AdapterView.OnItemClickListener +import android.widget.ArrayAdapter +import com.android.calendar.R +import com.android.calendar.Utils +import java.util.Arrays + +/** + * Activity which displays when the user wants to email guests from notifications. + * + * This presents the user with list if quick responses to be populated in an email + * to minimize typing. + * + */ +class QuickResponseActivity : ListActivity(), OnItemClickListener { + private var mResponses: Array<String?>? = null + @Override + protected override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + val intent: Intent? = getIntent() + if (intent == null) { + finish() + return + } + mEventId = intent?.getLongExtra(EXTRA_EVENT_ID, -1) as Long + if (mEventId == -1L) { + finish() + return + } + + // Set listener + getListView().setOnItemClickListener(this@QuickResponseActivity) + + // Populate responses + val responses: Array<String> = Utils.getQuickResponses(this) + Arrays.sort(responses) + + // Add "Custom response..." + mResponses = arrayOfNulls(responses.size + 1) + var i: Int + i = 0 + while (i < responses.size) { + mResponses!![i] = responses[i] + i++ + } + mResponses!![i] = getResources().getString(R.string.quick_response_custom_msg) + setListAdapter(ArrayAdapter<String>(this, R.layout.quick_response_item, + mResponses as Array<String?>)) + } + + // implements OnItemClickListener + @Override + override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + var body: String? = null + if (mResponses != null && position < mResponses!!.size - 1) { + body = mResponses!![position] + } + + // Start thread to query provider and send mail + QueryThread(mEventId, body).start() + } + + private inner class QueryThread internal constructor(var mEventId: Long, var mBody: String?) : + Thread() { + @Override + override fun run() { + } + } + + companion object { + private const val TAG = "QuickResponseActivity" + const val EXTRA_EVENT_ID = "eventId" + var mEventId: Long = 0 + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/month/MonthByWeekAdapter.java b/src/com/android/calendar/month/MonthByWeekAdapter.java deleted file mode 100644 index 45a1bea1..00000000 --- a/src/com/android/calendar/month/MonthByWeekAdapter.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar.month; - -import android.content.Context; -import android.content.res.Configuration; -import android.os.Handler; -import android.os.Message; -import android.text.format.Time; -import android.util.Log; -import android.view.GestureDetector; -import android.view.HapticFeedbackConstants; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.widget.AbsListView.LayoutParams; - -import com.android.calendar.CalendarController; -import com.android.calendar.CalendarController.EventType; -import com.android.calendar.CalendarController.ViewType; -import com.android.calendar.Event; -import com.android.calendar.R; -import com.android.calendar.Utils; - -import java.util.ArrayList; -import java.util.HashMap; - -public class MonthByWeekAdapter extends SimpleWeeksAdapter { - private static final String TAG = "MonthByWeekAdapter"; - - public static final String WEEK_PARAMS_IS_MINI = "mini_month"; - protected static int DEFAULT_QUERY_DAYS = 7 * 8; // 8 weeks - private static final long ANIMATE_TODAY_TIMEOUT = 1000; - - protected CalendarController mController; - protected String mHomeTimeZone; - protected Time mTempTime; - protected Time mToday; - protected int mFirstJulianDay; - protected int mQueryDays; - protected boolean mIsMiniMonth = true; - protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE; - private final boolean mShowAgendaWithMonth; - - protected ArrayList<ArrayList<Event>> mEventDayList = new ArrayList<ArrayList<Event>>(); - protected ArrayList<Event> mEvents = null; - - private boolean mAnimateToday = false; - private long mAnimateTime = 0; - - private Handler mEventDialogHandler; - - MonthWeekEventsView mClickedView; - MonthWeekEventsView mSingleTapUpView; - MonthWeekEventsView mLongClickedView; - - float mClickedXLocation; // Used to find which day was clicked - long mClickTime; // Used to calculate minimum click animation time - // Used to insure minimal time for seeing the click animation before switching views - private static final int mOnTapDelay = 100; - // Minimal time for a down touch action before stating the click animation, this insures that - // there is no click animation on flings - private static int mOnDownDelay; - private static int mTotalClickDelay; - // Minimal distance to move the finger in order to cancel the click animation - private static float mMovedPixelToCancel; - - public MonthByWeekAdapter(Context context, HashMap<String, Integer> params) { - super(context, params); - if (params.containsKey(WEEK_PARAMS_IS_MINI)) { - mIsMiniMonth = params.get(WEEK_PARAMS_IS_MINI) != 0; - } - mShowAgendaWithMonth = Utils.getConfigBool(context, R.bool.show_agenda_with_month); - ViewConfiguration vc = ViewConfiguration.get(context); - mOnDownDelay = ViewConfiguration.getTapTimeout(); - mMovedPixelToCancel = vc.getScaledTouchSlop(); - mTotalClickDelay = mOnDownDelay + mOnTapDelay; - } - - public void animateToday() { - mAnimateToday = true; - mAnimateTime = System.currentTimeMillis(); - } - - @Override - protected void init() { - super.init(); - mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); - mController = CalendarController.getInstance(mContext); - mHomeTimeZone = Utils.getTimeZone(mContext, null); - mSelectedDay.switchTimezone(mHomeTimeZone); - mToday = new Time(mHomeTimeZone); - mToday.setToNow(); - mTempTime = new Time(mHomeTimeZone); - } - - private void updateTimeZones() { - mSelectedDay.timezone = mHomeTimeZone; - mSelectedDay.normalize(true); - mToday.timezone = mHomeTimeZone; - mToday.setToNow(); - mTempTime.switchTimezone(mHomeTimeZone); - } - - @Override - public void setSelectedDay(Time selectedTime) { - mSelectedDay.set(selectedTime); - long millis = mSelectedDay.normalize(true); - mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay( - Time.getJulianDay(millis, mSelectedDay.gmtoff), mFirstDayOfWeek); - notifyDataSetChanged(); - } - - public void setEvents(int firstJulianDay, int numDays, ArrayList<Event> events) { - if (mIsMiniMonth) { - if (Log.isLoggable(TAG, Log.ERROR)) { - Log.e(TAG, "Attempted to set events for mini view. Events only supported in full" - + " view."); - } - return; - } - mEvents = events; - mFirstJulianDay = firstJulianDay; - mQueryDays = numDays; - // Create a new list, this is necessary since the weeks are referencing - // pieces of the old list - ArrayList<ArrayList<Event>> eventDayList = new ArrayList<ArrayList<Event>>(); - for (int i = 0; i < numDays; i++) { - eventDayList.add(new ArrayList<Event>()); - } - - if (events == null || events.size() == 0) { - if(Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "No events. Returning early--go schedule something fun."); - } - mEventDayList = eventDayList; - refresh(); - return; - } - - // Compute the new set of days with events - for (Event event : events) { - int startDay = event.startDay - mFirstJulianDay; - int endDay = event.endDay - mFirstJulianDay + 1; - if (startDay < numDays || endDay >= 0) { - if (startDay < 0) { - startDay = 0; - } - if (startDay > numDays) { - continue; - } - if (endDay < 0) { - continue; - } - if (endDay > numDays) { - endDay = numDays; - } - for (int j = startDay; j < endDay; j++) { - eventDayList.get(j).add(event); - } - } - } - if(Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Processed " + events.size() + " events."); - } - mEventDayList = eventDayList; - refresh(); - } - - @SuppressWarnings("unchecked") - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (mIsMiniMonth) { - return super.getView(position, convertView, parent); - } - MonthWeekEventsView v; - LayoutParams params = new LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - HashMap<String, Integer> drawingParams = null; - boolean isAnimatingToday = false; - if (convertView != null) { - v = (MonthWeekEventsView) convertView; - // Checking updateToday uses the current params instead of the new - // params, so this is assuming the view is relatively stable - if (mAnimateToday && v.updateToday(mSelectedDay.timezone)) { - long currentTime = System.currentTimeMillis(); - // If it's been too long since we tried to start the animation - // don't show it. This can happen if the user stops a scroll - // before reaching today. - if (currentTime - mAnimateTime > ANIMATE_TODAY_TIMEOUT) { - mAnimateToday = false; - mAnimateTime = 0; - } else { - isAnimatingToday = true; - // There is a bug that causes invalidates to not work some - // of the time unless we recreate the view. - v = new MonthWeekEventsView(mContext); - } - } else { - drawingParams = (HashMap<String, Integer>) v.getTag(); - } - } else { - v = new MonthWeekEventsView(mContext); - } - if (drawingParams == null) { - drawingParams = new HashMap<String, Integer>(); - } - drawingParams.clear(); - - v.setLayoutParams(params); - v.setClickable(true); - v.setOnTouchListener(this); - - int selectedDay = -1; - if (mSelectedWeek == position) { - selectedDay = mSelectedDay.weekDay; - } - - drawingParams.put(SimpleWeekView.VIEW_PARAMS_HEIGHT, - (parent.getHeight() + parent.getTop()) / mNumWeeks); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, mShowWeekNumber ? 1 : 0); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth); - drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ORIENTATION, mOrientation); - - if (isAnimatingToday) { - drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ANIMATE_TODAY, 1); - mAnimateToday = false; - } - - v.setWeekParams(drawingParams, mSelectedDay.timezone); - return v; - } - - @Override - protected void refresh() { - mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); - mShowWeekNumber = Utils.getShowWeekNumber(mContext); - mHomeTimeZone = Utils.getTimeZone(mContext, null); - mOrientation = mContext.getResources().getConfiguration().orientation; - updateTimeZones(); - notifyDataSetChanged(); - } - - @Override - protected void onDayTapped(Time day) { - setDayParameters(day); - if (mShowAgendaWithMonth || mIsMiniMonth) { - // If agenda view is visible with month view , refresh the views - // with the selected day's info - mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, - ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); - } else { - // Else , switch to the detailed view - mController.sendEvent(mContext, EventType.GO_TO, day, day, -1, - ViewType.DETAIL, - CalendarController.EXTRA_GOTO_DATE - | CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null); - } - } - - private void setDayParameters(Time day) { - day.timezone = mHomeTimeZone; - Time currTime = new Time(mHomeTimeZone); - currTime.set(mController.getTime()); - day.hour = currTime.hour; - day.minute = currTime.minute; - day.allDay = false; - day.normalize(true); - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (!(v instanceof MonthWeekEventsView)) { - return super.onTouch(v, event); - } - - int action = event.getAction(); - - // Event was tapped - switch to the detailed view making sure the click animation - // is done first. - if (mGestureDetector.onTouchEvent(event)) { - mSingleTapUpView = (MonthWeekEventsView) v; - long delay = System.currentTimeMillis() - mClickTime; - // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms - mListView.postDelayed(mDoSingleTapUp, - delay > mTotalClickDelay ? 0 : mTotalClickDelay - delay); - return true; - } else { - // Animate a click - on down: show the selected day in the "clicked" color. - // On Up/scroll/move/cancel: hide the "clicked" color. - switch (action) { - case MotionEvent.ACTION_DOWN: - mClickedView = (MonthWeekEventsView)v; - mClickedXLocation = event.getX(); - mClickTime = System.currentTimeMillis(); - mListView.postDelayed(mDoClick, mOnDownDelay); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_SCROLL: - case MotionEvent.ACTION_CANCEL: - clearClickedView((MonthWeekEventsView)v); - break; - case MotionEvent.ACTION_MOVE: - // No need to cancel on vertical movement, ACTION_SCROLL will do that. - if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) { - clearClickedView((MonthWeekEventsView)v); - } - break; - default: - break; - } - } - // Do not tell the frameworks we consumed the touch action so that fling actions can be - // processed by the fragment. - return false; - } - - /** - * This is here so we can identify events and process them - */ - protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { - @Override - public boolean onSingleTapUp(MotionEvent e) { - return true; - } - - @Override - public void onLongPress(MotionEvent e) { - if (mLongClickedView != null) { - Time day = mLongClickedView.getDayFromLocation(mClickedXLocation); - if (day != null) { - mLongClickedView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - Message message = new Message(); - message.obj = day; - } - mLongClickedView.clearClickedDay(); - mLongClickedView = null; - } - } - } - - // Clear the visual cues of the click animation and related running code. - private void clearClickedView(MonthWeekEventsView v) { - mListView.removeCallbacks(mDoClick); - synchronized(v) { - v.clearClickedDay(); - } - mClickedView = null; - } - - // Perform the tap animation in a runnable to allow a delay before showing the tap color. - // This is done to prevent a click animation when a fling is done. - private final Runnable mDoClick = new Runnable() { - @Override - public void run() { - if (mClickedView != null) { - synchronized(mClickedView) { - mClickedView.setClickedDay(mClickedXLocation); - } - mLongClickedView = mClickedView; - mClickedView = null; - // This is a workaround , sometimes the top item on the listview doesn't refresh on - // invalidate, so this forces a re-draw. - mListView.invalidate(); - } - } - }; - - // Performs the single tap operation: go to the tapped day. - // This is done in a runnable to allow the click animation to finish before switching views - private final Runnable mDoSingleTapUp = new Runnable() { - @Override - public void run() { - if (mSingleTapUpView != null) { - Time day = mSingleTapUpView.getDayFromLocation(mClickedXLocation); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Touched day at Row=" + mSingleTapUpView.mWeek + " day=" + day.toString()); - } - if (day != null) { - onDayTapped(day); - } - clearClickedView(mSingleTapUpView); - mSingleTapUpView = null; - } - } - }; -} diff --git a/src/com/android/calendar/month/MonthByWeekAdapter.kt b/src/com/android/calendar/month/MonthByWeekAdapter.kt new file mode 100644 index 00000000..c67b3562 --- /dev/null +++ b/src/com/android/calendar/month/MonthByWeekAdapter.kt @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2021 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.calendar.month + +import android.content.Context +import android.content.res.Configuration +import android.os.Handler +import android.os.Message +import android.text.format.Time +import android.util.Log +import android.view.GestureDetector +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.widget.AbsListView.LayoutParams +import com.android.calendar.CalendarController +import com.android.calendar.CalendarController.EventType +import com.android.calendar.CalendarController.ViewType +import com.android.calendar.Event +import com.android.calendar.R +import com.android.calendar.Utils +import java.util.ArrayList +import java.util.HashMap + +class MonthByWeekAdapter(context: Context?, params: HashMap<String?, Int?>) : + SimpleWeeksAdapter(context as Context, params) { + protected var mController: CalendarController? = null + protected var mHomeTimeZone: String? = null + protected var mTempTime: Time? = null + protected var mToday: Time? = null + protected var mFirstJulianDay = 0 + protected var mQueryDays = 0 + protected var mIsMiniMonth = true + protected var mOrientation: Int = Configuration.ORIENTATION_LANDSCAPE + private val mShowAgendaWithMonth: Boolean + protected var mEventDayList: ArrayList<ArrayList<Event>> = ArrayList<ArrayList<Event>>() + protected var mEvents: ArrayList<Event>? = null + private var mAnimateToday = false + private var mAnimateTime: Long = 0 + private val mEventDialogHandler: Handler? = null + var mClickedView: MonthWeekEventsView? = null + var mSingleTapUpView: MonthWeekEventsView? = null + var mLongClickedView: MonthWeekEventsView? = null + var mClickedXLocation = 0f // Used to find which day was clicked + var mClickTime: Long = 0 // Used to calculate minimum click animation time + + fun animateToday() { + mAnimateToday = true + mAnimateTime = System.currentTimeMillis() + } + + @Override + protected override fun init() { + super.init() + mGestureDetector = GestureDetector(mContext, CalendarGestureListener()) + mController = CalendarController.getInstance(mContext) + mHomeTimeZone = Utils.getTimeZone(mContext, null) + mSelectedDay?.switchTimezone(mHomeTimeZone) + mToday = Time(mHomeTimeZone) + mToday?.setToNow() + mTempTime = Time(mHomeTimeZone) + } + + private fun updateTimeZones() { + mSelectedDay!!.timezone = mHomeTimeZone + mSelectedDay?.normalize(true) + mToday!!.timezone = mHomeTimeZone + mToday?.setToNow() + mTempTime?.switchTimezone(mHomeTimeZone) + } + + @Override + override fun setSelectedDay(selectedTime: Time?) { + mSelectedDay?.set(selectedTime) + val millis: Long = mSelectedDay!!.normalize(true) + mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay( + Time.getJulianDay(millis, mSelectedDay!!.gmtoff), mFirstDayOfWeek + ) + notifyDataSetChanged() + } + + fun setEvents(firstJulianDay: Int, numDays: Int, events: ArrayList<Event>?) { + if (mIsMiniMonth) { + if (Log.isLoggable(TAG, Log.ERROR)) { + Log.e( + TAG, "Attempted to set events for mini view. Events only supported in full" + + " view." + ) + } + return + } + mEvents = events + mFirstJulianDay = firstJulianDay + mQueryDays = numDays + // Create a new list, this is necessary since the weeks are referencing + // pieces of the old list + val eventDayList: ArrayList<ArrayList<Event>> = ArrayList<ArrayList<Event>>() + for (i in 0 until numDays) { + eventDayList.add(ArrayList<Event>()) + } + if (events == null || events.size == 0) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "No events. Returning early--go schedule something fun.") + } + mEventDayList = eventDayList + refresh() + return + } + + // Compute the new set of days with events + for (event in events) { + var startDay: Int = event.startDay - mFirstJulianDay + var endDay: Int = event.endDay - mFirstJulianDay + 1 + if (startDay < numDays || endDay >= 0) { + if (startDay < 0) { + startDay = 0 + } + if (startDay > numDays) { + continue + } + if (endDay < 0) { + continue + } + if (endDay > numDays) { + endDay = numDays + } + for (j in startDay until endDay) { + eventDayList.get(j).add(event) + } + } + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Processed " + events.size.toString() + " events.") + } + mEventDayList = eventDayList + refresh() + } + + @SuppressWarnings("unchecked") + @Override + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + if (mIsMiniMonth) { + return super.getView(position, convertView, parent) + } + var v: MonthWeekEventsView + val params = LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT + ) + var drawingParams: HashMap<String?, Int?>? = null + var isAnimatingToday = false + if (convertView != null) { + v = convertView as MonthWeekEventsView + // Checking updateToday uses the current params instead of the new + // params, so this is assuming the view is relatively stable + if (mAnimateToday && v.updateToday(mSelectedDay!!.timezone)) { + val currentTime: Long = System.currentTimeMillis() + // If it's been too long since we tried to start the animation + // don't show it. This can happen if the user stops a scroll + // before reaching today. + if (currentTime - mAnimateTime > ANIMATE_TODAY_TIMEOUT) { + mAnimateToday = false + mAnimateTime = 0 + } else { + isAnimatingToday = true + // There is a bug that causes invalidates to not work some + // of the time unless we recreate the view. + v = MonthWeekEventsView(mContext) + } + } else { + drawingParams = v.getTag() as HashMap<String?, Int?> + } + } else { + v = MonthWeekEventsView(mContext) + } + if (drawingParams == null) { + drawingParams = HashMap<String?, Int?>() + } + drawingParams.clear() + v.setLayoutParams(params) + v.setClickable(true) + v.setOnTouchListener(this) + var selectedDay = -1 + if (mSelectedWeek === position) { + selectedDay = mSelectedDay!!.weekDay + } + drawingParams.put( + SimpleWeekView.VIEW_PARAMS_HEIGHT, + (parent.getHeight() + parent.getTop()) / mNumWeeks + ) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, if (mShowWeekNumber) 1 else 0) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth) + drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ORIENTATION, mOrientation) + if (isAnimatingToday) { + drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ANIMATE_TODAY, 1) + mAnimateToday = false + } + v.setWeekParams(drawingParams, mSelectedDay!!.timezone) + return v + } + + @Override + internal override fun refresh() { + mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext) + mShowWeekNumber = Utils.getShowWeekNumber(mContext) + mHomeTimeZone = Utils.getTimeZone(mContext, null) + mOrientation = mContext.getResources().getConfiguration().orientation + updateTimeZones() + notifyDataSetChanged() + } + + @Override + protected override fun onDayTapped(day: Time) { + setDayParameters(day) + if (mShowAgendaWithMonth || mIsMiniMonth) { + // If agenda view is visible with month view , refresh the views + // with the selected day's info + mController?.sendEvent( + mContext as Object?, EventType.GO_TO, day, day, -1, + ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null + ) + } else { + // Else , switch to the detailed view + mController?.sendEvent( + mContext as Object?, EventType.GO_TO, day, day, -1, + ViewType.DETAIL, CalendarController.EXTRA_GOTO_DATE + or CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null + ) + } + } + + private fun setDayParameters(day: Time) { + day.timezone = mHomeTimeZone + val currTime = Time(mHomeTimeZone) + currTime.set(mController!!.time as Long) + day.hour = currTime.hour + day.minute = currTime.minute + day.allDay = false + day.normalize(true) + } + + @Override + override fun onTouch(v: View, event: MotionEvent): Boolean { + if (v !is MonthWeekEventsView) { + return super.onTouch(v, event) + } + val action: Int = event!!.getAction() + + // Event was tapped - switch to the detailed view making sure the click animation + // is done first. + if (mGestureDetector!!.onTouchEvent(event)) { + mSingleTapUpView = v as MonthWeekEventsView? + val delay: Long = System.currentTimeMillis() - mClickTime + // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms + mListView?.postDelayed( + mDoSingleTapUp, + if (delay > mTotalClickDelay) 0 else mTotalClickDelay - delay + ) + return true + } else { + // Animate a click - on down: show the selected day in the "clicked" color. + // On Up/scroll/move/cancel: hide the "clicked" color. + when (action) { + MotionEvent.ACTION_DOWN -> { + mClickedView = v as MonthWeekEventsView + mClickedXLocation = event.getX() + mClickTime = System.currentTimeMillis() + mListView?.postDelayed(mDoClick, mOnDownDelay.toLong()) + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_SCROLL, MotionEvent.ACTION_CANCEL -> + clearClickedView( + v as MonthWeekEventsView? + ) + MotionEvent.ACTION_MOVE -> // No need to cancel on vertical movement, + // ACTION_SCROLL will do that. + if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) { + clearClickedView(v as MonthWeekEventsView?) + } + else -> { + } + } + } + // Do not tell the frameworks we consumed the touch action so that fling actions can be + // processed by the fragment. + return false + } + + /** + * This is here so we can identify events and process them + */ + protected inner class CalendarGestureListener : GestureDetector.SimpleOnGestureListener() { + @Override + override fun onSingleTapUp(e: MotionEvent): Boolean { + return true + } + + @Override + override fun onLongPress(e: MotionEvent) { + if (mLongClickedView != null) { + val day: Time? = mLongClickedView?.getDayFromLocation(mClickedXLocation) + if (day != null) { + mLongClickedView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + val message = Message() + message.obj = day + } + mLongClickedView?.clearClickedDay() + mLongClickedView = null + } + } + } + + // Clear the visual cues of the click animation and related running code. + private fun clearClickedView(v: MonthWeekEventsView?) { + mListView?.removeCallbacks(mDoClick) + synchronized(v as Any) { v?.clearClickedDay() } + mClickedView = null + } + + // Perform the tap animation in a runnable to allow a delay before showing the tap color. + // This is done to prevent a click animation when a fling is done. + private val mDoClick: Runnable = object : Runnable { + @Override + override fun run() { + if (mClickedView != null) { + synchronized(mClickedView as MonthWeekEventsView) { + mClickedView?.setClickedDay(mClickedXLocation) } + mLongClickedView = mClickedView + mClickedView = null + // This is a workaround , sometimes the top item on the listview doesn't refresh on + // invalidate, so this forces a re-draw. + mListView?.invalidate() + } + } + } + + // Performs the single tap operation: go to the tapped day. + // This is done in a runnable to allow the click animation to finish before switching views + private val mDoSingleTapUp: Runnable = object : Runnable { + @Override + override fun run() { + if (mSingleTapUpView != null) { + val day: Time? = mSingleTapUpView?.getDayFromLocation(mClickedXLocation) + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d( + TAG, + "Touched day at Row=" + mSingleTapUpView?.mWeek?.toString() + + " day=" + day?.toString() + ) + } + if (day != null) { + onDayTapped(day) + } + clearClickedView(mSingleTapUpView) + mSingleTapUpView = null + } + } + } + + companion object { + private const val TAG = "MonthByWeekAdapter" + const val WEEK_PARAMS_IS_MINI = "mini_month" + protected var DEFAULT_QUERY_DAYS = 7 * 8 // 8 weeks + private const val ANIMATE_TODAY_TIMEOUT: Long = 1000 + + // Used to insure minimal time for seeing the click animation before switching views + private const val mOnTapDelay = 100 + + // Minimal time for a down touch action before stating the click animation, this ensures + // that there is no click animation on flings + private var mOnDownDelay: Int = 0 + private var mTotalClickDelay: Int = 0 + + // Minimal distance to move the finger in order to cancel the click animation + private var mMovedPixelToCancel: Float = 0f + } + + init { + if (params.containsKey(WEEK_PARAMS_IS_MINI)) { + mIsMiniMonth = params.get(WEEK_PARAMS_IS_MINI) != 0 + } + mShowAgendaWithMonth = Utils.getConfigBool(context as Context, + R.bool.show_agenda_with_month) + val vc: ViewConfiguration = ViewConfiguration.get(context) + mOnDownDelay = ViewConfiguration.getTapTimeout() + mMovedPixelToCancel = vc.getScaledTouchSlop().toFloat() + mTotalClickDelay = mOnDownDelay + mOnTapDelay + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/month/MonthByWeekFragment.java b/src/com/android/calendar/month/MonthByWeekFragment.java deleted file mode 100644 index f8a518d3..00000000 --- a/src/com/android/calendar/month/MonthByWeekFragment.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar.month; - -import android.app.Activity; -import android.app.FragmentManager; -import android.app.LoaderManager; -import android.content.ContentUris; -import android.content.CursorLoader; -import android.content.Loader; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.drawable.StateListDrawable; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.provider.CalendarContract.Attendees; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Instances; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnTouchListener; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; - -import com.android.calendar.CalendarController; -import com.android.calendar.CalendarController.EventInfo; -import com.android.calendar.CalendarController.EventType; -import com.android.calendar.CalendarController.ViewType; -import com.android.calendar.Event; -import com.android.calendar.R; -import com.android.calendar.Utils; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.HashMap; -import java.util.List; - -public class MonthByWeekFragment extends SimpleDayPickerFragment implements - CalendarController.EventHandler, LoaderManager.LoaderCallbacks<Cursor>, OnScrollListener, - OnTouchListener { - private static final String TAG = "MonthFragment"; - private static final String TAG_EVENT_DIALOG = "event_dialog"; - - // Selection and selection args for adding event queries - private static final String WHERE_CALENDARS_VISIBLE = Calendars.VISIBLE + "=1"; - private static final String INSTANCES_SORT_ORDER = Instances.START_DAY + "," - + Instances.START_MINUTE + "," + Instances.TITLE; - protected static boolean mShowDetailsInMonth = false; - - protected float mMinimumTwoMonthFlingVelocity; - protected boolean mIsMiniMonth; - protected boolean mHideDeclined; - - protected int mFirstLoadedJulianDay; - protected int mLastLoadedJulianDay; - - private static final int WEEKS_BUFFER = 1; - // How long to wait after scroll stops before starting the loader - // Using scroll duration because scroll state changes don't update - // correctly when a scroll is triggered programmatically. - private static final int LOADER_DELAY = 200; - // The minimum time between requeries of the data if the db is - // changing - private static final int LOADER_THROTTLE_DELAY = 500; - - private CursorLoader mLoader; - private Uri mEventUri; - private final Time mDesiredDay = new Time(); - - private volatile boolean mShouldLoad = true; - private boolean mUserScrolled = false; - - private int mEventsLoadingDelay; - private boolean mShowCalendarControls; - private boolean mIsDetached; - - private final Runnable mTZUpdater = new Runnable() { - @Override - public void run() { - String tz = Utils.getTimeZone(mContext, mTZUpdater); - mSelectedDay.timezone = tz; - mSelectedDay.normalize(true); - mTempTime.timezone = tz; - mFirstDayOfMonth.timezone = tz; - mFirstDayOfMonth.normalize(true); - mFirstVisibleDay.timezone = tz; - mFirstVisibleDay.normalize(true); - if (mAdapter != null) { - mAdapter.refresh(); - } - } - }; - - - private final Runnable mUpdateLoader = new Runnable() { - @Override - public void run() { - synchronized (this) { - if (!mShouldLoad || mLoader == null) { - return; - } - // Stop any previous loads while we update the uri - stopLoader(); - - // Start the loader again - mEventUri = updateUri(); - - mLoader.setUri(mEventUri); - mLoader.startLoading(); - mLoader.onContentChanged(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Started loader with uri: " + mEventUri); - } - } - } - }; - // Used to load the events when a delay is needed - Runnable mLoadingRunnable = new Runnable() { - @Override - public void run() { - if (!mIsDetached) { - mLoader = (CursorLoader) getLoaderManager().initLoader(0, null, - MonthByWeekFragment.this); - } - } - }; - - - /** - * Updates the uri used by the loader according to the current position of - * the listview. - * - * @return The new Uri to use - */ - private Uri updateUri() { - SimpleWeekView child = (SimpleWeekView) mListView.getChildAt(0); - if (child != null) { - int julianDay = child.getFirstJulianDay(); - mFirstLoadedJulianDay = julianDay; - } - // -1 to ensure we get all day events from any time zone - mTempTime.setJulianDay(mFirstLoadedJulianDay - 1); - long start = mTempTime.toMillis(true); - mLastLoadedJulianDay = mFirstLoadedJulianDay + (mNumWeeks + 2 * WEEKS_BUFFER) * 7; - // +1 to ensure we get all day events from any time zone - mTempTime.setJulianDay(mLastLoadedJulianDay + 1); - long end = mTempTime.toMillis(true); - - // Create a new uri with the updated times - Uri.Builder builder = Instances.CONTENT_URI.buildUpon(); - ContentUris.appendId(builder, start); - ContentUris.appendId(builder, end); - return builder.build(); - } - - // Extract range of julian days from URI - private void updateLoadedDays() { - List<String> pathSegments = mEventUri.getPathSegments(); - int size = pathSegments.size(); - if (size <= 2) { - return; - } - long first = Long.parseLong(pathSegments.get(size - 2)); - long last = Long.parseLong(pathSegments.get(size - 1)); - mTempTime.set(first); - mFirstLoadedJulianDay = Time.getJulianDay(first, mTempTime.gmtoff); - mTempTime.set(last); - mLastLoadedJulianDay = Time.getJulianDay(last, mTempTime.gmtoff); - } - - protected String updateWhere() { - // TODO fix selection/selection args after b/3206641 is fixed - String where = WHERE_CALENDARS_VISIBLE; - if (mHideDeclined || !mShowDetailsInMonth) { - where += " AND " + Instances.SELF_ATTENDEE_STATUS + "!=" - + Attendees.ATTENDEE_STATUS_DECLINED; - } - return where; - } - - private void stopLoader() { - synchronized (mUpdateLoader) { - mHandler.removeCallbacks(mUpdateLoader); - if (mLoader != null) { - mLoader.stopLoading(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Stopped loader from loading"); - } - } - } - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - mTZUpdater.run(); - if (mAdapter != null) { - mAdapter.setSelectedDay(mSelectedDay); - } - mIsDetached = false; - - ViewConfiguration viewConfig = ViewConfiguration.get(activity); - mMinimumTwoMonthFlingVelocity = viewConfig.getScaledMaximumFlingVelocity() / 2; - Resources res = activity.getResources(); - mShowCalendarControls = Utils.getConfigBool(activity, R.bool.show_calendar_controls); - // Synchronized the loading time of the month's events with the animation of the - // calendar controls. - if (mShowCalendarControls) { - mEventsLoadingDelay = res.getInteger(R.integer.calendar_controls_animation_time); - } - mShowDetailsInMonth = res.getBoolean(R.bool.show_details_in_month); - } - - @Override - public void onDetach() { - mIsDetached = true; - super.onDetach(); - if (mShowCalendarControls) { - if (mListView != null) { - mListView.removeCallbacks(mLoadingRunnable); - } - } - } - - @Override - protected void setUpAdapter() { - mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); - mShowWeekNumber = Utils.getShowWeekNumber(mContext); - - HashMap<String, Integer> weekParams = new HashMap<String, Integer>(); - weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks); - weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, mShowWeekNumber ? 1 : 0); - weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek); - weekParams.put(MonthByWeekAdapter.WEEK_PARAMS_IS_MINI, mIsMiniMonth ? 1 : 0); - weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY, - Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff)); - weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_DAYS_PER_WEEK, mDaysPerWeek); - if (mAdapter == null) { - mAdapter = new MonthByWeekAdapter(getActivity(), weekParams); - mAdapter.registerDataSetObserver(mObserver); - } else { - mAdapter.updateParams(weekParams); - } - mAdapter.notifyDataSetChanged(); - } - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v; - if (mIsMiniMonth) { - v = inflater.inflate(R.layout.month_by_week, container, false); - } else { - v = inflater.inflate(R.layout.full_month_by_week, container, false); - } - mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names); - return v; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mListView.setSelector(new StateListDrawable()); - mListView.setOnTouchListener(this); - - if (!mIsMiniMonth) { - mListView.setBackgroundColor(getResources().getColor(R.color.month_bgcolor)); - } - - // To get a smoother transition when showing this fragment, delay loading of events until - // the fragment is expended fully and the calendar controls are gone. - if (mShowCalendarControls) { - mListView.postDelayed(mLoadingRunnable, mEventsLoadingDelay); - } else { - mLoader = (CursorLoader) getLoaderManager().initLoader(0, null, this); - } - mAdapter.setListView(mListView); - } - - public MonthByWeekFragment() { - this(System.currentTimeMillis(), true); - } - - public MonthByWeekFragment(long initialTime, boolean isMiniMonth) { - super(initialTime); - mIsMiniMonth = isMiniMonth; - } - - @Override - protected void setUpHeader() { - if (mIsMiniMonth) { - super.setUpHeader(); - return; - } - - mDayLabels = new String[7]; - for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { - mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(i, - DateUtils.LENGTH_MEDIUM).toUpperCase(); - } - } - - // TODO - @Override - public Loader<Cursor> onCreateLoader(int id, Bundle args) { - if (mIsMiniMonth) { - return null; - } - CursorLoader loader; - synchronized (mUpdateLoader) { - mFirstLoadedJulianDay = - Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff) - - (mNumWeeks * 7 / 2); - mEventUri = updateUri(); - String where = updateWhere(); - - loader = new CursorLoader( - getActivity(), mEventUri, Event.EVENT_PROJECTION, where, - null /* WHERE_CALENDARS_SELECTED_ARGS */, INSTANCES_SORT_ORDER); - loader.setUpdateThrottle(LOADER_THROTTLE_DELAY); - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Returning new loader with uri: " + mEventUri); - } - return loader; - } - - @Override - public void doResumeUpdates() { - mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); - mShowWeekNumber = Utils.getShowWeekNumber(mContext); - boolean prevHideDeclined = mHideDeclined; - mHideDeclined = Utils.getHideDeclinedEvents(mContext); - if (prevHideDeclined != mHideDeclined && mLoader != null) { - mLoader.setSelection(updateWhere()); - } - mDaysPerWeek = Utils.getDaysPerWeek(mContext); - updateHeader(); - mAdapter.setSelectedDay(mSelectedDay); - mTZUpdater.run(); - mTodayUpdater.run(); - goTo(mSelectedDay.toMillis(true), false, true, false); - } - - @Override - public void onLoadFinished(Loader<Cursor> loader, Cursor data) { - synchronized (mUpdateLoader) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Found " + data.getCount() + " cursor entries for uri " + mEventUri); - } - CursorLoader cLoader = (CursorLoader) loader; - if (mEventUri == null) { - mEventUri = cLoader.getUri(); - updateLoadedDays(); - } - if (cLoader.getUri().compareTo(mEventUri) != 0) { - // We've started a new query since this loader ran so ignore the - // result - return; - } - ArrayList<Event> events = new ArrayList<Event>(); - Event.buildEventsFromCursor( - events, data, mContext, mFirstLoadedJulianDay, mLastLoadedJulianDay); - ((MonthByWeekAdapter) mAdapter).setEvents(mFirstLoadedJulianDay, - mLastLoadedJulianDay - mFirstLoadedJulianDay + 1, events); - } - } - - @Override - public void onLoaderReset(Loader<Cursor> loader) { - } - - @Override - public void eventsChanged() { - // TODO remove this after b/3387924 is resolved - if (mLoader != null) { - mLoader.forceLoad(); - } - } - - @Override - public long getSupportedEventTypes() { - return EventType.GO_TO | EventType.EVENTS_CHANGED; - } - - @Override - public void handleEvent(EventInfo event) { - if (event.eventType == EventType.GO_TO) { - boolean animate = true; - if (mDaysPerWeek * mNumWeeks * 2 < Math.abs( - Time.getJulianDay(event.selectedTime.toMillis(true), event.selectedTime.gmtoff) - - Time.getJulianDay(mFirstVisibleDay.toMillis(true), mFirstVisibleDay.gmtoff) - - mDaysPerWeek * mNumWeeks / 2)) { - animate = false; - } - mDesiredDay.set(event.selectedTime); - mDesiredDay.normalize(true); - boolean animateToday = (event.extraLong & CalendarController.EXTRA_GOTO_TODAY) != 0; - boolean delayAnimation = goTo(event.selectedTime.toMillis(true), animate, true, false); - if (animateToday) { - // If we need to flash today start the animation after any - // movement from listView has ended. - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - ((MonthByWeekAdapter) mAdapter).animateToday(); - mAdapter.notifyDataSetChanged(); - } - }, delayAnimation ? GOTO_SCROLL_DURATION : 0); - } - } else if (event.eventType == EventType.EVENTS_CHANGED) { - eventsChanged(); - } - } - - @Override - protected void setMonthDisplayed(Time time, boolean updateHighlight) { - super.setMonthDisplayed(time, updateHighlight); - if (!mIsMiniMonth) { - boolean useSelected = false; - if (time.year == mDesiredDay.year && time.month == mDesiredDay.month) { - mSelectedDay.set(mDesiredDay); - mAdapter.setSelectedDay(mDesiredDay); - useSelected = true; - } else { - mSelectedDay.set(time); - mAdapter.setSelectedDay(time); - } - CalendarController controller = CalendarController.getInstance(mContext); - if (mSelectedDay.minute >= 30) { - mSelectedDay.minute = 30; - } else { - mSelectedDay.minute = 0; - } - long newTime = mSelectedDay.normalize(true); - if (newTime != controller.getTime() && mUserScrolled) { - long offset = useSelected ? 0 : DateUtils.WEEK_IN_MILLIS * mNumWeeks / 3; - controller.setTime(newTime + offset); - } - controller.sendEvent(this, EventType.UPDATE_TITLE, time, time, time, -1, - ViewType.CURRENT, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY - | DateUtils.FORMAT_SHOW_YEAR, null, null); - } - } - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - - synchronized (mUpdateLoader) { - if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) { - mShouldLoad = false; - stopLoader(); - mDesiredDay.setToNow(); - } else { - mHandler.removeCallbacks(mUpdateLoader); - mShouldLoad = true; - mHandler.postDelayed(mUpdateLoader, LOADER_DELAY); - } - } - if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { - mUserScrolled = true; - } - - mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - mDesiredDay.setToNow(); - return false; - } -} diff --git a/src/com/android/calendar/month/MonthByWeekFragment.kt b/src/com/android/calendar/month/MonthByWeekFragment.kt new file mode 100644 index 00000000..9fe9fe49 --- /dev/null +++ b/src/com/android/calendar/month/MonthByWeekFragment.kt @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2021 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.calendar.month + +import android.app.Activity +import android.app.LoaderManager +import android.content.ContentUris +import android.content.CursorLoader +import android.content.Loader +import android.content.res.Resources +import android.database.Cursor +import android.graphics.drawable.StateListDrawable +import android.net.Uri +import android.os.Bundle +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Instances +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.View.OnTouchListener +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.widget.AbsListView +import android.widget.AbsListView.OnScrollListener + +import com.android.calendar.CalendarController +import com.android.calendar.CalendarController.EventInfo +import com.android.calendar.CalendarController.EventType +import com.android.calendar.CalendarController.ViewType +import com.android.calendar.Event +import com.android.calendar.R +import com.android.calendar.Utils + +import java.util.ArrayList +import java.util.Calendar +import java.util.HashMap + +class MonthByWeekFragment @JvmOverloads constructor( + initialTime: Long = System.currentTimeMillis(), + protected var mIsMiniMonth: Boolean = true +) : SimpleDayPickerFragment(initialTime), CalendarController.EventHandler, + LoaderManager.LoaderCallbacks<Cursor?>, OnScrollListener, OnTouchListener { + protected var mMinimumTwoMonthFlingVelocity = 0f + protected var mHideDeclined = false + protected var mFirstLoadedJulianDay = 0 + protected var mLastLoadedJulianDay = 0 + private var mLoader: CursorLoader? = null + private var mEventUri: Uri? = null + private val mDesiredDay: Time = Time() + + @Volatile + private var mShouldLoad = true + private var mUserScrolled = false + private var mEventsLoadingDelay = 0 + private var mShowCalendarControls = false + private var mIsDetached = false + private val mTZUpdater: Runnable = object : Runnable { + @Override + override fun run() { + val tz: String? = Utils.getTimeZone(mContext, this) + mSelectedDay.timezone = tz + mSelectedDay.normalize(true) + mTempTime.timezone = tz + mFirstDayOfMonth.timezone = tz + mFirstDayOfMonth.normalize(true) + mFirstVisibleDay.timezone = tz + mFirstVisibleDay.normalize(true) + if (mAdapter != null) { + mAdapter?.refresh() + } + } + } + private val mUpdateLoader: Runnable = object : Runnable { + @Override + override fun run() { + synchronized(this) { + if (!mShouldLoad || mLoader == null) { + return + } + // Stop any previous loads while we update the uri + stopLoader() + + // Start the loader again + mEventUri = updateUri() + mLoader?.setUri(mEventUri) + mLoader?.startLoading() + mLoader?.onContentChanged() + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Started loader with uri: $mEventUri") + } + } + } + } + + // Used to load the events when a delay is needed + var mLoadingRunnable: Runnable = object : Runnable { + @Override + override fun run() { + if (!mIsDetached) { + mLoader = getLoaderManager().initLoader( + 0, null, + this@MonthByWeekFragment + ) as? CursorLoader + } + } + } + + /** + * Updates the uri used by the loader according to the current position of + * the listview. + * + * @return The new Uri to use + */ + private fun updateUri(): Uri { + val child: SimpleWeekView? = mListView?.getChildAt(0) as? SimpleWeekView + if (child != null) { + val julianDay: Int = child?.getFirstJulianDay() + mFirstLoadedJulianDay = julianDay + } + // -1 to ensure we get all day events from any time zone + mTempTime.setJulianDay(mFirstLoadedJulianDay - 1) + val start: Long = mTempTime.toMillis(true) + mLastLoadedJulianDay = mFirstLoadedJulianDay + (mNumWeeks + 2 * WEEKS_BUFFER) * 7 + // +1 to ensure we get all day events from any time zone + mTempTime.setJulianDay(mLastLoadedJulianDay + 1) + val end: Long = mTempTime.toMillis(true) + + // Create a new uri with the updated times + val builder: Uri.Builder = Instances.CONTENT_URI.buildUpon() + ContentUris.appendId(builder, start) + ContentUris.appendId(builder, end) + return builder.build() + } + + // Extract range of julian days from URI + private fun updateLoadedDays() { + val pathSegments = mEventUri?.getPathSegments() + val size: Int = pathSegments?.size as Int + if (size <= 2) { + return + } + val first: Long = (pathSegments!![size - 2])?.toLong() as Long + val last: Long = (pathSegments!![size - 1])?.toLong() as Long + mTempTime.set(first) + mFirstLoadedJulianDay = Time.getJulianDay(first, mTempTime.gmtoff) + mTempTime.set(last) + mLastLoadedJulianDay = Time.getJulianDay(last, mTempTime.gmtoff) + } + + protected fun updateWhere(): String { + // TODO fix selection/selection args after b/3206641 is fixed + var where = WHERE_CALENDARS_VISIBLE + if (mHideDeclined || !mShowDetailsInMonth) { + where += (" AND " + Instances.SELF_ATTENDEE_STATUS.toString() + "!=" + + Attendees.ATTENDEE_STATUS_DECLINED) + } + return where + } + + private fun stopLoader() { + synchronized(mUpdateLoader) { + mHandler.removeCallbacks(mUpdateLoader) + if (mLoader != null) { + mLoader?.stopLoading() + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Stopped loader from loading") + } + } + } + } + + @Override + override fun onAttach(activity: Activity) { + super.onAttach(activity) + mTZUpdater.run() + if (mAdapter != null) { + mAdapter?.setSelectedDay(mSelectedDay) + } + mIsDetached = false + val viewConfig: ViewConfiguration = ViewConfiguration.get(activity) + mMinimumTwoMonthFlingVelocity = viewConfig.getScaledMaximumFlingVelocity().toFloat() / 2f + val res: Resources = activity.getResources() + mShowCalendarControls = Utils.getConfigBool(activity, R.bool.show_calendar_controls) + // Synchronized the loading time of the month's events with the animation of the + // calendar controls. + if (mShowCalendarControls) { + mEventsLoadingDelay = res.getInteger(R.integer.calendar_controls_animation_time) + } + mShowDetailsInMonth = res.getBoolean(R.bool.show_details_in_month) + } + + @Override + override fun onDetach() { + mIsDetached = true + super.onDetach() + if (mShowCalendarControls) { + if (mListView != null) { + mListView?.removeCallbacks(mLoadingRunnable) + } + } + } + + @Override + protected override fun setUpAdapter() { + mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext) + mShowWeekNumber = Utils.getShowWeekNumber(mContext) + val weekParams = HashMap<String?, Int?>() + weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks) + weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, if (mShowWeekNumber) 1 else 0) + weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek) + weekParams?.put(MonthByWeekAdapter.WEEK_PARAMS_IS_MINI, if (mIsMiniMonth) 1 else 0) + weekParams?.put( + SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY, + Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff) + ) + weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_DAYS_PER_WEEK, mDaysPerWeek) + if (mAdapter == null) { + mAdapter = MonthByWeekAdapter(getActivity(), weekParams) as SimpleWeeksAdapter? + mAdapter?.registerDataSetObserver(mObserver) + } else { + mAdapter?.updateParams(weekParams) + } + mAdapter?.notifyDataSetChanged() + } + + @Override + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val v: View + v = if (mIsMiniMonth) { + inflater.inflate(R.layout.month_by_week, container, false) + } else { + inflater.inflate(R.layout.full_month_by_week, container, false) + } + mDayNamesHeader = v.findViewById(R.id.day_names) as? ViewGroup + return v + } + + @Override + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + mListView?.setSelector(StateListDrawable()) + mListView?.setOnTouchListener(this) + if (!mIsMiniMonth) { + mListView?.setBackgroundColor(getResources().getColor(R.color.month_bgcolor)) + } + + // To get a smoother transition when showing this fragment, delay loading of events until + // the fragment is expended fully and the calendar controls are gone. + if (mShowCalendarControls) { + mListView?.postDelayed(mLoadingRunnable, mEventsLoadingDelay.toLong()) + } else { + mLoader = getLoaderManager().initLoader(0, null, this) as? CursorLoader + } + mAdapter?.setListView(mListView) + } + + @Override + protected override fun setUpHeader() { + if (mIsMiniMonth) { + super.setUpHeader() + return + } + mDayLabels = arrayOfNulls<String>(7) + for (i in Calendar.SUNDAY..Calendar.SATURDAY) { + mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString( + i, + DateUtils.LENGTH_MEDIUM + ).toUpperCase() + } + } + + // TODO + @Override + override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor?>? { + if (mIsMiniMonth) { + return null + } + var loader: CursorLoader? + synchronized(mUpdateLoader) { + mFirstLoadedJulianDay = + (Time.getJulianDay(mSelectedDay.toMillis(true), mSelectedDay.gmtoff) - + mNumWeeks * 7 / 2) + mEventUri = updateUri() + val where = updateWhere() + loader = CursorLoader( + getActivity(), mEventUri, Event.EVENT_PROJECTION, where, + null /* WHERE_CALENDARS_SELECTED_ARGS */, INSTANCES_SORT_ORDER + ) + loader?.setUpdateThrottle(LOADER_THROTTLE_DELAY.toLong()) + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Returning new loader with uri: $mEventUri") + } + return loader + } + + @Override + override fun doResumeUpdates() { + mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext) + mShowWeekNumber = Utils.getShowWeekNumber(mContext) + val prevHideDeclined = mHideDeclined + mHideDeclined = Utils.getHideDeclinedEvents(mContext) + if (prevHideDeclined != mHideDeclined && mLoader != null) { + mLoader?.setSelection(updateWhere()) + } + mDaysPerWeek = Utils.getDaysPerWeek(mContext) + updateHeader() + mAdapter?.setSelectedDay(mSelectedDay) + mTZUpdater.run() + mTodayUpdater.run() + goTo(mSelectedDay.toMillis(true), false, true, false) + } + + @Override + override fun onLoadFinished(loader: Loader<Cursor?>?, data: Cursor?) { + synchronized(mUpdateLoader) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d( + TAG, + "Found " + data?.getCount()?.toString() + " cursor entries for uri " + + mEventUri + ) + } + val cLoader: CursorLoader = loader as CursorLoader + if (mEventUri == null) { + mEventUri = cLoader.getUri() + updateLoadedDays() + } + if (cLoader.getUri().compareTo(mEventUri) !== 0) { + // We've started a new query since this loader ran so ignore the + // result + return + } + val events: ArrayList<Event?>? = ArrayList<Event?>() + Event.buildEventsFromCursor( + events, data, mContext, mFirstLoadedJulianDay, mLastLoadedJulianDay + ) + (mAdapter as MonthByWeekAdapter).setEvents( + mFirstLoadedJulianDay, + mLastLoadedJulianDay - mFirstLoadedJulianDay + 1, events as ArrayList<Event>? + ) + } + } + + @Override + override fun onLoaderReset(loader: Loader<Cursor?>?) { + } + + @Override + override fun eventsChanged() { + // TODO remove this after b/3387924 is resolved + if (mLoader != null) { + mLoader?.forceLoad() + } + } + + @get:Override override val supportedEventTypes: Long + get() = EventType.GO_TO or EventType.EVENTS_CHANGED + + @Override + override fun handleEvent(event: CalendarController.EventInfo?) { + if (event?.eventType === EventType.GO_TO) { + var animate = true + if (mDaysPerWeek * mNumWeeks * 2 < Math.abs( + Time.getJulianDay(event?.selectedTime?.toMillis(true) as Long, + event?.selectedTime?.gmtoff as Long) - + Time.getJulianDay(mFirstVisibleDay?.toMillis(true) as Long, + mFirstVisibleDay?.gmtoff as Long) - + mDaysPerWeek * mNumWeeks / 2L + ) + ) { + animate = false + } + mDesiredDay.set(event?.selectedTime) + mDesiredDay.normalize(true) + val animateToday = event?.extraLong and + CalendarController.EXTRA_GOTO_TODAY.toLong() != 0L + val delayAnimation: Boolean = + goTo(event?.selectedTime?.toMillis(true)?.toLong() as Long, + animate, true, false) + if (animateToday) { + // If we need to flash today start the animation after any + // movement from listView has ended. + mHandler.postDelayed(object : Runnable { + @Override + override fun run() { + (mAdapter as? MonthByWeekAdapter)?.animateToday() + mAdapter?.notifyDataSetChanged() + } + }, if (delayAnimation) GOTO_SCROLL_DURATION.toLong() else 0L) + } + } else if (event?.eventType == EventType.EVENTS_CHANGED) { + eventsChanged() + } + } + + @Override + protected override fun setMonthDisplayed(time: Time, updateHighlight: Boolean) { + super.setMonthDisplayed(time, updateHighlight) + if (!mIsMiniMonth) { + var useSelected = false + if (time.year == mDesiredDay.year && time.month == mDesiredDay.month) { + mSelectedDay.set(mDesiredDay) + mAdapter?.setSelectedDay(mDesiredDay) + useSelected = true + } else { + mSelectedDay.set(time) + mAdapter?.setSelectedDay(time) + } + val controller: CalendarController? = CalendarController.getInstance(mContext) + if (mSelectedDay.minute >= 30) { + mSelectedDay.minute = 30 + } else { + mSelectedDay.minute = 0 + } + val newTime: Long = mSelectedDay.normalize(true) + if (newTime != controller?.time && mUserScrolled) { + val offset: Long = + if (useSelected) 0 else DateUtils.WEEK_IN_MILLIS * mNumWeeks / 3.toLong() + controller?.time = (newTime + offset) + } + controller?.sendEvent( + this as Object?, EventType.UPDATE_TITLE, time, time, time, -1, + ViewType.CURRENT, DateUtils.FORMAT_SHOW_DATE.toLong() or + DateUtils.FORMAT_NO_MONTH_DAY.toLong() or + DateUtils.FORMAT_SHOW_YEAR.toLong(), null, null + ) + } + } + + @Override + override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) { + synchronized(mUpdateLoader) { + if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) { + mShouldLoad = false + stopLoader() + mDesiredDay.setToNow() + } else { + mHandler.removeCallbacks(mUpdateLoader) + mShouldLoad = true + mHandler.postDelayed(mUpdateLoader, LOADER_DELAY.toLong()) + } + } + if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + mUserScrolled = true + } + mScrollStateChangedRunnable.doScrollStateChange(view, scrollState) + } + + @Override + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + mDesiredDay.setToNow() + return false + } + + companion object { + private const val TAG = "MonthFragment" + private const val TAG_EVENT_DIALOG = "event_dialog" + + // Selection and selection args for adding event queries + private val WHERE_CALENDARS_VISIBLE: String = Calendars.VISIBLE.toString() + "=1" + private val INSTANCES_SORT_ORDER: String = (Instances.START_DAY.toString() + "," + + Instances.START_MINUTE + "," + Instances.TITLE) + protected var mShowDetailsInMonth = false + private const val WEEKS_BUFFER = 1 + + // How long to wait after scroll stops before starting the loader + // Using scroll duration because scroll state changes don't update + // correctly when a scroll is triggered programmatically. + private const val LOADER_DELAY = 200 + + // The minimum time between requeries of the data if the db is + // changing + private const val LOADER_THROTTLE_DELAY = 500 + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/month/MonthListView.java b/src/com/android/calendar/month/MonthListView.java deleted file mode 100644 index f2621ccb..00000000 --- a/src/com/android/calendar/month/MonthListView.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.calendar.month; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.widget.ListView; - -import com.android.calendar.Utils; - -public class MonthListView extends ListView { - - private static final String TAG = "MonthListView"; - - public MonthListView(Context context) { - super(context); - } - - public MonthListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public MonthListView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - return super.onTouchEvent(ev); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - return super.onInterceptTouchEvent(ev); - } -} diff --git a/src/com/android/calendar/month/MonthListView.kt b/src/com/android/calendar/month/MonthListView.kt new file mode 100644 index 00000000..1facb4c0 --- /dev/null +++ b/src/com/android/calendar/month/MonthListView.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 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.calendar.month + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.ListView +import com.android.calendar.Utils + +class MonthListView : ListView { + constructor(context: Context?) : super(context) {} + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : + super(context, attrs, defStyle) {} + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {} + + @Override + override fun onTouchEvent(ev: MotionEvent?): Boolean { + return super.onTouchEvent(ev) + } + + @Override + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return super.onInterceptTouchEvent(ev) + } + + companion object { + private const val TAG = "MonthListView" + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/month/MonthWeekEventsView.java b/src/com/android/calendar/month/MonthWeekEventsView.java deleted file mode 100644 index e1c78c67..00000000 --- a/src/com/android/calendar/month/MonthWeekEventsView.java +++ /dev/null @@ -1,1110 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar.month; - -import com.android.calendar.Event; -import com.android.calendar.R; -import com.android.calendar.Utils; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.app.Service; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Paint.Align; -import android.graphics.Paint.Style; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.provider.CalendarContract.Attendees; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; -import android.view.MotionEvent; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Formatter; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; - -public class MonthWeekEventsView extends SimpleWeekView { - - private static final String TAG = "MonthView"; - - private static final boolean DEBUG_LAYOUT = false; - - public static final String VIEW_PARAMS_ORIENTATION = "orientation"; - public static final String VIEW_PARAMS_ANIMATE_TODAY = "animate_today"; - - /* NOTE: these are not constants, and may be multiplied by a scale factor */ - private static int TEXT_SIZE_MONTH_NUMBER = 32; - private static int TEXT_SIZE_EVENT = 12; - private static int TEXT_SIZE_EVENT_TITLE = 14; - private static int TEXT_SIZE_MORE_EVENTS = 12; - private static int TEXT_SIZE_MONTH_NAME = 14; - private static int TEXT_SIZE_WEEK_NUM = 12; - - private static int DNA_MARGIN = 4; - private static int DNA_ALL_DAY_HEIGHT = 4; - private static int DNA_MIN_SEGMENT_HEIGHT = 4; - private static int DNA_WIDTH = 8; - private static int DNA_ALL_DAY_WIDTH = 32; - private static int DNA_SIDE_PADDING = 6; - private static int CONFLICT_COLOR = Color.BLACK; - private static int EVENT_TEXT_COLOR = Color.WHITE; - - private static int DEFAULT_EDGE_SPACING = 0; - private static int SIDE_PADDING_MONTH_NUMBER = 4; - private static int TOP_PADDING_MONTH_NUMBER = 4; - private static int TOP_PADDING_WEEK_NUMBER = 4; - private static int SIDE_PADDING_WEEK_NUMBER = 20; - private static int DAY_SEPARATOR_OUTER_WIDTH = 0; - private static int DAY_SEPARATOR_INNER_WIDTH = 1; - private static int DAY_SEPARATOR_VERTICAL_LENGTH = 53; - private static int DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT = 64; - private static int MIN_WEEK_WIDTH = 50; - - private static int EVENT_X_OFFSET_LANDSCAPE = 38; - private static int EVENT_Y_OFFSET_LANDSCAPE = 8; - private static int EVENT_Y_OFFSET_PORTRAIT = 7; - private static int EVENT_SQUARE_WIDTH = 10; - private static int EVENT_SQUARE_BORDER = 2; - private static int EVENT_LINE_PADDING = 2; - private static int EVENT_RIGHT_PADDING = 4; - private static int EVENT_BOTTOM_PADDING = 3; - - private static int TODAY_HIGHLIGHT_WIDTH = 2; - - private static int SPACING_WEEK_NUMBER = 24; - private static boolean mInitialized = false; - private static boolean mShowDetailsInMonth; - - protected Time mToday = new Time(); - protected boolean mHasToday = false; - protected int mTodayIndex = -1; - protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE; - protected List<ArrayList<Event>> mEvents = null; - protected ArrayList<Event> mUnsortedEvents = null; - HashMap<Integer, Utils.DNAStrand> mDna = null; - // This is for drawing the outlines around event chips and supports up to 10 - // events being drawn on each day. The code will expand this if necessary. - protected FloatRef mEventOutlines = new FloatRef(10 * 4 * 4 * 7); - - - - protected static StringBuilder mStringBuilder = new StringBuilder(50); - // TODO recreate formatter when locale changes - protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); - - protected Paint mMonthNamePaint; - protected TextPaint mEventPaint; - protected TextPaint mSolidBackgroundEventPaint; - protected TextPaint mFramedEventPaint; - protected TextPaint mDeclinedEventPaint; - protected TextPaint mEventExtrasPaint; - protected TextPaint mEventDeclinedExtrasPaint; - protected Paint mWeekNumPaint; - protected Paint mDNAAllDayPaint; - protected Paint mDNATimePaint; - protected Paint mEventSquarePaint; - - - protected Drawable mTodayDrawable; - - protected int mMonthNumHeight; - protected int mMonthNumAscentHeight; - protected int mEventHeight; - protected int mEventAscentHeight; - protected int mExtrasHeight; - protected int mExtrasAscentHeight; - protected int mExtrasDescent; - protected int mWeekNumAscentHeight; - - protected int mMonthBGColor; - protected int mMonthBGOtherColor; - protected int mMonthBGTodayColor; - protected int mMonthNumColor; - protected int mMonthNumOtherColor; - protected int mMonthNumTodayColor; - protected int mMonthNameColor; - protected int mMonthNameOtherColor; - protected int mMonthEventColor; - protected int mMonthDeclinedEventColor; - protected int mMonthDeclinedExtrasColor; - protected int mMonthEventExtraColor; - protected int mMonthEventOtherColor; - protected int mMonthEventExtraOtherColor; - protected int mMonthWeekNumColor; - protected int mMonthBusyBitsBgColor; - protected int mMonthBusyBitsBusyTimeColor; - protected int mMonthBusyBitsConflictTimeColor; - private int mClickedDayIndex = -1; - private int mClickedDayColor; - private static final int mClickedAlpha = 128; - - protected int mEventChipOutlineColor = 0xFFFFFFFF; - protected int mDaySeparatorInnerColor; - protected int mTodayAnimateColor; - - private boolean mAnimateToday; - private int mAnimateTodayAlpha = 0; - private ObjectAnimator mTodayAnimator = null; - - private final TodayAnimatorListener mAnimatorListener = new TodayAnimatorListener(); - - class TodayAnimatorListener extends AnimatorListenerAdapter { - private volatile Animator mAnimator = null; - private volatile boolean mFadingIn = false; - - @Override - public void onAnimationEnd(Animator animation) { - synchronized (this) { - if (mAnimator != animation) { - animation.removeAllListeners(); - animation.cancel(); - return; - } - if (mFadingIn) { - if (mTodayAnimator != null) { - mTodayAnimator.removeAllListeners(); - mTodayAnimator.cancel(); - } - mTodayAnimator = ObjectAnimator.ofInt(MonthWeekEventsView.this, - "animateTodayAlpha", 255, 0); - mAnimator = mTodayAnimator; - mFadingIn = false; - mTodayAnimator.addListener(this); - mTodayAnimator.setDuration(600); - mTodayAnimator.start(); - } else { - mAnimateToday = false; - mAnimateTodayAlpha = 0; - mAnimator.removeAllListeners(); - mAnimator = null; - mTodayAnimator = null; - invalidate(); - } - } - } - - public void setAnimator(Animator animation) { - mAnimator = animation; - } - - public void setFadingIn(boolean fadingIn) { - mFadingIn = fadingIn; - } - - } - - private int[] mDayXs; - - /** - * This provides a reference to a float array which allows for easy size - * checking and reallocation. Used for drawing lines. - */ - private class FloatRef { - float[] array; - - public FloatRef(int size) { - array = new float[size]; - } - - public void ensureSize(int newSize) { - if (newSize >= array.length) { - // Add enough space for 7 more boxes to be drawn - array = Arrays.copyOf(array, newSize + 16 * 7); - } - } - } - - /** - * Shows up as an error if we don't include this. - */ - public MonthWeekEventsView(Context context) { - super(context); - } - - // Sets the list of events for this week. Takes a sorted list of arrays - // divided up by day for generating the large month version and the full - // arraylist sorted by start time to generate the dna version. - public void setEvents(List<ArrayList<Event>> sortedEvents, ArrayList<Event> unsortedEvents) { - setEvents(sortedEvents); - // The MIN_WEEK_WIDTH is a hack to prevent the view from trying to - // generate dna bits before its width has been fixed. - createDna(unsortedEvents); - } - - /** - * Sets up the dna bits for the view. This will return early if the view - * isn't in a state that will create a valid set of dna yet (such as the - * views width not being set correctly yet). - */ - public void createDna(ArrayList<Event> unsortedEvents) { - if (unsortedEvents == null || mWidth <= MIN_WEEK_WIDTH || getContext() == null) { - // Stash the list of events for use when this view is ready, or - // just clear it if a null set has been passed to this view - mUnsortedEvents = unsortedEvents; - mDna = null; - return; - } else { - // clear the cached set of events since we're ready to build it now - mUnsortedEvents = null; - } - // Create the drawing coordinates for dna - if (!mShowDetailsInMonth) { - int numDays = mEvents.size(); - int effectiveWidth = mWidth - mPadding * 2; - if (mShowWeekNum) { - effectiveWidth -= SPACING_WEEK_NUMBER; - } - DNA_ALL_DAY_WIDTH = effectiveWidth / numDays - 2 * DNA_SIDE_PADDING; - mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH); - mDayXs = new int[numDays]; - for (int day = 0; day < numDays; day++) { - mDayXs[day] = computeDayLeftPosition(day) + DNA_WIDTH / 2 + DNA_SIDE_PADDING; - - } - - int top = DAY_SEPARATOR_INNER_WIDTH + DNA_MARGIN + DNA_ALL_DAY_HEIGHT + 1; - int bottom = mHeight - DNA_MARGIN; - mDna = Utils.createDNAStrands(mFirstJulianDay, unsortedEvents, top, bottom, - DNA_MIN_SEGMENT_HEIGHT, mDayXs, getContext()); - } - } - - public void setEvents(List<ArrayList<Event>> sortedEvents) { - mEvents = sortedEvents; - if (sortedEvents == null) { - return; - } - if (sortedEvents.size() != mNumDays) { - if (Log.isLoggable(TAG, Log.ERROR)) { - Log.wtf(TAG, "Events size must be same as days displayed: size=" - + sortedEvents.size() + " days=" + mNumDays); - } - mEvents = null; - return; - } - } - - protected void loadColors(Context context) { - Resources res = context.getResources(); - mMonthWeekNumColor = res.getColor(R.color.month_week_num_color); - mMonthNumColor = res.getColor(R.color.month_day_number); - mMonthNumOtherColor = res.getColor(R.color.month_day_number_other); - mMonthNumTodayColor = res.getColor(R.color.month_today_number); - mMonthNameColor = mMonthNumColor; - mMonthNameOtherColor = mMonthNumOtherColor; - mMonthEventColor = res.getColor(R.color.month_event_color); - mMonthDeclinedEventColor = res.getColor(R.color.agenda_item_declined_color); - mMonthDeclinedExtrasColor = res.getColor(R.color.agenda_item_where_declined_text_color); - mMonthEventExtraColor = res.getColor(R.color.month_event_extra_color); - mMonthEventOtherColor = res.getColor(R.color.month_event_other_color); - mMonthEventExtraOtherColor = res.getColor(R.color.month_event_extra_other_color); - mMonthBGTodayColor = res.getColor(R.color.month_today_bgcolor); - mMonthBGOtherColor = res.getColor(R.color.month_other_bgcolor); - mMonthBGColor = res.getColor(R.color.month_bgcolor); - mDaySeparatorInnerColor = res.getColor(R.color.month_grid_lines); - mTodayAnimateColor = res.getColor(R.color.today_highlight_color); - mClickedDayColor = res.getColor(R.color.day_clicked_background_color); - mTodayDrawable = res.getDrawable(R.drawable.today_blue_week_holo_light); - } - - /** - * Sets up the text and style properties for painting. Override this if you - * want to use a different paint. - */ - @Override - protected void initView() { - super.initView(); - - if (!mInitialized) { - Resources resources = getContext().getResources(); - mShowDetailsInMonth = Utils.getConfigBool(getContext(), R.bool.show_details_in_month); - TEXT_SIZE_EVENT_TITLE = resources.getInteger(R.integer.text_size_event_title); - TEXT_SIZE_MONTH_NUMBER = resources.getInteger(R.integer.text_size_month_number); - SIDE_PADDING_MONTH_NUMBER = resources.getInteger(R.integer.month_day_number_margin); - CONFLICT_COLOR = resources.getColor(R.color.month_dna_conflict_time_color); - EVENT_TEXT_COLOR = resources.getColor(R.color.calendar_event_text_color); - if (mScale != 1) { - TOP_PADDING_MONTH_NUMBER *= mScale; - TOP_PADDING_WEEK_NUMBER *= mScale; - SIDE_PADDING_MONTH_NUMBER *= mScale; - SIDE_PADDING_WEEK_NUMBER *= mScale; - SPACING_WEEK_NUMBER *= mScale; - TEXT_SIZE_MONTH_NUMBER *= mScale; - TEXT_SIZE_EVENT *= mScale; - TEXT_SIZE_EVENT_TITLE *= mScale; - TEXT_SIZE_MORE_EVENTS *= mScale; - TEXT_SIZE_MONTH_NAME *= mScale; - TEXT_SIZE_WEEK_NUM *= mScale; - DAY_SEPARATOR_OUTER_WIDTH *= mScale; - DAY_SEPARATOR_INNER_WIDTH *= mScale; - DAY_SEPARATOR_VERTICAL_LENGTH *= mScale; - DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT *= mScale; - EVENT_X_OFFSET_LANDSCAPE *= mScale; - EVENT_Y_OFFSET_LANDSCAPE *= mScale; - EVENT_Y_OFFSET_PORTRAIT *= mScale; - EVENT_SQUARE_WIDTH *= mScale; - EVENT_SQUARE_BORDER *= mScale; - EVENT_LINE_PADDING *= mScale; - EVENT_BOTTOM_PADDING *= mScale; - EVENT_RIGHT_PADDING *= mScale; - DNA_MARGIN *= mScale; - DNA_WIDTH *= mScale; - DNA_ALL_DAY_HEIGHT *= mScale; - DNA_MIN_SEGMENT_HEIGHT *= mScale; - DNA_SIDE_PADDING *= mScale; - DEFAULT_EDGE_SPACING *= mScale; - DNA_ALL_DAY_WIDTH *= mScale; - TODAY_HIGHLIGHT_WIDTH *= mScale; - } - if (!mShowDetailsInMonth) { - TOP_PADDING_MONTH_NUMBER += DNA_ALL_DAY_HEIGHT + DNA_MARGIN; - } - mInitialized = true; - } - mPadding = DEFAULT_EDGE_SPACING; - loadColors(getContext()); - // TODO modify paint properties depending on isMini - - mMonthNumPaint = new Paint(); - mMonthNumPaint.setFakeBoldText(false); - mMonthNumPaint.setAntiAlias(true); - mMonthNumPaint.setTextSize(TEXT_SIZE_MONTH_NUMBER); - mMonthNumPaint.setColor(mMonthNumColor); - mMonthNumPaint.setStyle(Style.FILL); - mMonthNumPaint.setTextAlign(Align.RIGHT); - mMonthNumPaint.setTypeface(Typeface.DEFAULT); - - mMonthNumAscentHeight = (int) (-mMonthNumPaint.ascent() + 0.5f); - mMonthNumHeight = (int) (mMonthNumPaint.descent() - mMonthNumPaint.ascent() + 0.5f); - - mEventPaint = new TextPaint(); - mEventPaint.setFakeBoldText(true); - mEventPaint.setAntiAlias(true); - mEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE); - mEventPaint.setColor(mMonthEventColor); - - mSolidBackgroundEventPaint = new TextPaint(mEventPaint); - mSolidBackgroundEventPaint.setColor(EVENT_TEXT_COLOR); - mFramedEventPaint = new TextPaint(mSolidBackgroundEventPaint); - - mDeclinedEventPaint = new TextPaint(); - mDeclinedEventPaint.setFakeBoldText(true); - mDeclinedEventPaint.setAntiAlias(true); - mDeclinedEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE); - mDeclinedEventPaint.setColor(mMonthDeclinedEventColor); - - mEventAscentHeight = (int) (-mEventPaint.ascent() + 0.5f); - mEventHeight = (int) (mEventPaint.descent() - mEventPaint.ascent() + 0.5f); - - mEventExtrasPaint = new TextPaint(); - mEventExtrasPaint.setFakeBoldText(false); - mEventExtrasPaint.setAntiAlias(true); - mEventExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER); - mEventExtrasPaint.setTextSize(TEXT_SIZE_EVENT); - mEventExtrasPaint.setColor(mMonthEventExtraColor); - mEventExtrasPaint.setStyle(Style.FILL); - mEventExtrasPaint.setTextAlign(Align.LEFT); - mExtrasHeight = (int)(mEventExtrasPaint.descent() - mEventExtrasPaint.ascent() + 0.5f); - mExtrasAscentHeight = (int)(-mEventExtrasPaint.ascent() + 0.5f); - mExtrasDescent = (int)(mEventExtrasPaint.descent() + 0.5f); - - mEventDeclinedExtrasPaint = new TextPaint(); - mEventDeclinedExtrasPaint.setFakeBoldText(false); - mEventDeclinedExtrasPaint.setAntiAlias(true); - mEventDeclinedExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER); - mEventDeclinedExtrasPaint.setTextSize(TEXT_SIZE_EVENT); - mEventDeclinedExtrasPaint.setColor(mMonthDeclinedExtrasColor); - mEventDeclinedExtrasPaint.setStyle(Style.FILL); - mEventDeclinedExtrasPaint.setTextAlign(Align.LEFT); - - mWeekNumPaint = new Paint(); - mWeekNumPaint.setFakeBoldText(false); - mWeekNumPaint.setAntiAlias(true); - mWeekNumPaint.setTextSize(TEXT_SIZE_WEEK_NUM); - mWeekNumPaint.setColor(mWeekNumColor); - mWeekNumPaint.setStyle(Style.FILL); - mWeekNumPaint.setTextAlign(Align.RIGHT); - - mWeekNumAscentHeight = (int) (-mWeekNumPaint.ascent() + 0.5f); - - mDNAAllDayPaint = new Paint(); - mDNATimePaint = new Paint(); - mDNATimePaint.setColor(mMonthBusyBitsBusyTimeColor); - mDNATimePaint.setStyle(Style.FILL_AND_STROKE); - mDNATimePaint.setStrokeWidth(DNA_WIDTH); - mDNATimePaint.setAntiAlias(false); - mDNAAllDayPaint.setColor(mMonthBusyBitsConflictTimeColor); - mDNAAllDayPaint.setStyle(Style.FILL_AND_STROKE); - mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH); - mDNAAllDayPaint.setAntiAlias(false); - - mEventSquarePaint = new Paint(); - mEventSquarePaint.setStrokeWidth(EVENT_SQUARE_BORDER); - mEventSquarePaint.setAntiAlias(false); - - if (DEBUG_LAYOUT) { - Log.d("EXTRA", "mScale=" + mScale); - Log.d("EXTRA", "mMonthNumPaint ascent=" + mMonthNumPaint.ascent() - + " descent=" + mMonthNumPaint.descent() + " int height=" + mMonthNumHeight); - Log.d("EXTRA", "mEventPaint ascent=" + mEventPaint.ascent() - + " descent=" + mEventPaint.descent() + " int height=" + mEventHeight - + " int ascent=" + mEventAscentHeight); - Log.d("EXTRA", "mEventExtrasPaint ascent=" + mEventExtrasPaint.ascent() - + " descent=" + mEventExtrasPaint.descent() + " int height=" + mExtrasHeight); - Log.d("EXTRA", "mWeekNumPaint ascent=" + mWeekNumPaint.ascent() - + " descent=" + mWeekNumPaint.descent()); - } - } - - @Override - public void setWeekParams(HashMap<String, Integer> params, String tz) { - super.setWeekParams(params, tz); - - if (params.containsKey(VIEW_PARAMS_ORIENTATION)) { - mOrientation = params.get(VIEW_PARAMS_ORIENTATION); - } - - updateToday(tz); - mNumCells = mNumDays + 1; - - if (params.containsKey(VIEW_PARAMS_ANIMATE_TODAY) && mHasToday) { - synchronized (mAnimatorListener) { - if (mTodayAnimator != null) { - mTodayAnimator.removeAllListeners(); - mTodayAnimator.cancel(); - } - mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha", - Math.max(mAnimateTodayAlpha, 80), 255); - mTodayAnimator.setDuration(150); - mAnimatorListener.setAnimator(mTodayAnimator); - mAnimatorListener.setFadingIn(true); - mTodayAnimator.addListener(mAnimatorListener); - mAnimateToday = true; - mTodayAnimator.start(); - } - } - } - - /** - * @param tz - */ - public boolean updateToday(String tz) { - mToday.timezone = tz; - mToday.setToNow(); - mToday.normalize(true); - int julianToday = Time.getJulianDay(mToday.toMillis(false), mToday.gmtoff); - if (julianToday >= mFirstJulianDay && julianToday < mFirstJulianDay + mNumDays) { - mHasToday = true; - mTodayIndex = julianToday - mFirstJulianDay; - } else { - mHasToday = false; - mTodayIndex = -1; - } - return mHasToday; - } - - public void setAnimateTodayAlpha(int alpha) { - mAnimateTodayAlpha = alpha; - invalidate(); - } - - @Override - protected void onDraw(Canvas canvas) { - drawBackground(canvas); - drawWeekNums(canvas); - drawDaySeparators(canvas); - if (mHasToday && mAnimateToday) { - drawToday(canvas); - } - if (mShowDetailsInMonth) { - drawEvents(canvas); - } else { - if (mDna == null && mUnsortedEvents != null) { - createDna(mUnsortedEvents); - } - drawDNA(canvas); - } - drawClick(canvas); - } - - protected void drawToday(Canvas canvas) { - r.top = DAY_SEPARATOR_INNER_WIDTH + (TODAY_HIGHLIGHT_WIDTH / 2); - r.bottom = mHeight - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f); - p.setStyle(Style.STROKE); - p.setStrokeWidth(TODAY_HIGHLIGHT_WIDTH); - r.left = computeDayLeftPosition(mTodayIndex) + (TODAY_HIGHLIGHT_WIDTH / 2); - r.right = computeDayLeftPosition(mTodayIndex + 1) - - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f); - p.setColor(mTodayAnimateColor | (mAnimateTodayAlpha << 24)); - canvas.drawRect(r, p); - p.setStyle(Style.FILL); - } - - // TODO move into SimpleWeekView - // Computes the x position for the left side of the given day - private int computeDayLeftPosition(int day) { - int effectiveWidth = mWidth; - int x = 0; - int xOffset = 0; - if (mShowWeekNum) { - xOffset = SPACING_WEEK_NUMBER + mPadding; - effectiveWidth -= xOffset; - } - x = day * effectiveWidth / mNumDays + xOffset; - return x; - } - - @Override - protected void drawDaySeparators(Canvas canvas) { - float lines[] = new float[8 * 4]; - int count = 6 * 4; - int wkNumOffset = 0; - int i = 0; - if (mShowWeekNum) { - // This adds the first line separating the week number - int xOffset = SPACING_WEEK_NUMBER + mPadding; - count += 4; - lines[i++] = xOffset; - lines[i++] = 0; - lines[i++] = xOffset; - lines[i++] = mHeight; - wkNumOffset++; - } - count += 4; - lines[i++] = 0; - lines[i++] = 0; - lines[i++] = mWidth; - lines[i++] = 0; - int y0 = 0; - int y1 = mHeight; - - while (i < count) { - int x = computeDayLeftPosition(i / 4 - wkNumOffset); - lines[i++] = x; - lines[i++] = y0; - lines[i++] = x; - lines[i++] = y1; - } - p.setColor(mDaySeparatorInnerColor); - p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH); - canvas.drawLines(lines, 0, count, p); - } - - @Override - protected void drawBackground(Canvas canvas) { - int i = 0; - int offset = 0; - r.top = DAY_SEPARATOR_INNER_WIDTH; - r.bottom = mHeight; - if (mShowWeekNum) { - i++; - offset++; - } - if (!mOddMonth[i]) { - while (++i < mOddMonth.length && !mOddMonth[i]) - ; - r.right = computeDayLeftPosition(i - offset); - r.left = 0; - p.setColor(mMonthBGOtherColor); - canvas.drawRect(r, p); - // compute left edge for i, set up r, draw - } else if (!mOddMonth[(i = mOddMonth.length - 1)]) { - while (--i >= offset && !mOddMonth[i]) - ; - i++; - // compute left edge for i, set up r, draw - r.right = mWidth; - r.left = computeDayLeftPosition(i - offset); - p.setColor(mMonthBGOtherColor); - canvas.drawRect(r, p); - } - if (mHasToday) { - p.setColor(mMonthBGTodayColor); - r.left = computeDayLeftPosition(mTodayIndex); - r.right = computeDayLeftPosition(mTodayIndex + 1); - canvas.drawRect(r, p); - } - } - - // Draw the "clicked" color on the tapped day - private void drawClick(Canvas canvas) { - if (mClickedDayIndex != -1) { - int alpha = p.getAlpha(); - p.setColor(mClickedDayColor); - p.setAlpha(mClickedAlpha); - r.left = computeDayLeftPosition(mClickedDayIndex); - r.right = computeDayLeftPosition(mClickedDayIndex + 1); - r.top = DAY_SEPARATOR_INNER_WIDTH; - r.bottom = mHeight; - canvas.drawRect(r, p); - p.setAlpha(alpha); - } - } - - @Override - protected void drawWeekNums(Canvas canvas) { - int y; - - int i = 0; - int offset = -1; - int todayIndex = mTodayIndex; - int x = 0; - int numCount = mNumDays; - if (mShowWeekNum) { - x = SIDE_PADDING_WEEK_NUMBER + mPadding; - y = mWeekNumAscentHeight + TOP_PADDING_WEEK_NUMBER; - canvas.drawText(mDayNumbers[0], x, y, mWeekNumPaint); - numCount++; - i++; - todayIndex++; - offset++; - - } - - y = mMonthNumAscentHeight + TOP_PADDING_MONTH_NUMBER; - - boolean isFocusMonth = mFocusDay[i]; - boolean isBold = false; - mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor); - for (; i < numCount; i++) { - if (mHasToday && todayIndex == i) { - mMonthNumPaint.setColor(mMonthNumTodayColor); - mMonthNumPaint.setFakeBoldText(isBold = true); - if (i + 1 < numCount) { - // Make sure the color will be set back on the next - // iteration - isFocusMonth = !mFocusDay[i + 1]; - } - } else if (mFocusDay[i] != isFocusMonth) { - isFocusMonth = mFocusDay[i]; - mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor); - } - x = computeDayLeftPosition(i - offset) - (SIDE_PADDING_MONTH_NUMBER); - canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint); - if (isBold) { - mMonthNumPaint.setFakeBoldText(isBold = false); - } - } - } - - protected void drawEvents(Canvas canvas) { - if (mEvents == null) { - return; - } - - int day = -1; - for (ArrayList<Event> eventDay : mEvents) { - day++; - if (eventDay == null || eventDay.size() == 0) { - continue; - } - int ySquare; - int xSquare = computeDayLeftPosition(day) + SIDE_PADDING_MONTH_NUMBER + 1; - int rightEdge = computeDayLeftPosition(day + 1); - - if (mOrientation == Configuration.ORIENTATION_PORTRAIT) { - ySquare = EVENT_Y_OFFSET_PORTRAIT + mMonthNumHeight + TOP_PADDING_MONTH_NUMBER; - rightEdge -= SIDE_PADDING_MONTH_NUMBER + 1; - } else { - ySquare = EVENT_Y_OFFSET_LANDSCAPE; - rightEdge -= EVENT_X_OFFSET_LANDSCAPE; - } - - // Determine if everything will fit when time ranges are shown. - boolean showTimes = true; - Iterator<Event> iter = eventDay.iterator(); - int yTest = ySquare; - while (iter.hasNext()) { - Event event = iter.next(); - int newY = drawEvent(canvas, event, xSquare, yTest, rightEdge, iter.hasNext(), - showTimes, /*doDraw*/ false); - if (newY == yTest) { - showTimes = false; - break; - } - yTest = newY; - } - - int eventCount = 0; - iter = eventDay.iterator(); - while (iter.hasNext()) { - Event event = iter.next(); - int newY = drawEvent(canvas, event, xSquare, ySquare, rightEdge, iter.hasNext(), - showTimes, /*doDraw*/ true); - if (newY == ySquare) { - break; - } - eventCount++; - ySquare = newY; - } - - int remaining = eventDay.size() - eventCount; - if (remaining > 0) { - drawMoreEvents(canvas, remaining, xSquare); - } - } - } - - protected int addChipOutline(FloatRef lines, int count, int x, int y) { - lines.ensureSize(count + 16); - // top of box - lines.array[count++] = x; - lines.array[count++] = y; - lines.array[count++] = x + EVENT_SQUARE_WIDTH; - lines.array[count++] = y; - // right side of box - lines.array[count++] = x + EVENT_SQUARE_WIDTH; - lines.array[count++] = y; - lines.array[count++] = x + EVENT_SQUARE_WIDTH; - lines.array[count++] = y + EVENT_SQUARE_WIDTH; - // left side of box - lines.array[count++] = x; - lines.array[count++] = y; - lines.array[count++] = x; - lines.array[count++] = y + EVENT_SQUARE_WIDTH + 1; - // bottom of box - lines.array[count++] = x; - lines.array[count++] = y + EVENT_SQUARE_WIDTH; - lines.array[count++] = x + EVENT_SQUARE_WIDTH + 1; - lines.array[count++] = y + EVENT_SQUARE_WIDTH; - - return count; - } - - /** - * Attempts to draw the given event. Returns the y for the next event or the - * original y if the event will not fit. An event is considered to not fit - * if the event and its extras won't fit or if there are more events and the - * more events line would not fit after drawing this event. - * - * @param canvas the canvas to draw on - * @param event the event to draw - * @param x the top left corner for this event's color chip - * @param y the top left corner for this event's color chip - * @param rightEdge the rightmost point we're allowed to draw on (exclusive) - * @param moreEvents indicates whether additional events will follow this one - * @param showTimes if set, a second line with a time range will be displayed for non-all-day - * events - * @param doDraw if set, do the actual drawing; otherwise this just computes the height - * and returns - * @return the y for the next event or the original y if it won't fit - */ - protected int drawEvent(Canvas canvas, Event event, int x, int y, int rightEdge, - boolean moreEvents, boolean showTimes, boolean doDraw) { - /* - * Vertical layout: - * (top of box) - * a. EVENT_Y_OFFSET_LANDSCAPE or portrait equivalent - * b. Event title: mEventHeight for a normal event, + 2xBORDER_SPACE for all-day event - * c. [optional] Time range (mExtrasHeight) - * d. EVENT_LINE_PADDING - * - * Repeat (b,c,d) as needed and space allows. If we have more events than fit, we need - * to leave room for something like "+2" at the bottom: - * - * e. "+ more" line (mExtrasHeight) - * - * f. EVENT_BOTTOM_PADDING (overlaps EVENT_LINE_PADDING) - * (bottom of box) - */ - final int BORDER_SPACE = EVENT_SQUARE_BORDER + 1; // want a 1-pixel gap inside border - final int STROKE_WIDTH_ADJ = EVENT_SQUARE_BORDER / 2; // adjust bounds for stroke width - boolean allDay = event.allDay; - int eventRequiredSpace = mEventHeight; - if (allDay) { - // Add a few pixels for the box we draw around all-day events. - eventRequiredSpace += BORDER_SPACE * 2; - } else if (showTimes) { - // Need room for the "1pm - 2pm" line. - eventRequiredSpace += mExtrasHeight; - } - int reservedSpace = EVENT_BOTTOM_PADDING; // leave a bit of room at the bottom - if (moreEvents) { - // More events follow. Leave a bit of space between events. - eventRequiredSpace += EVENT_LINE_PADDING; - - // Make sure we have room for the "+ more" line. (The "+ more" line is expected - // to be <= the height of an event line, so we won't show "+1" when we could be - // showing the event.) - reservedSpace += mExtrasHeight; - } - - if (y + eventRequiredSpace + reservedSpace > mHeight) { - // Not enough space, return original y - return y; - } else if (!doDraw) { - return y + eventRequiredSpace; - } - - boolean isDeclined = event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED; - int color = event.color; - if (isDeclined) { - color = Utils.getDeclinedColorFromColor(color); - } - - int textX, textY, textRightEdge; - - if (allDay) { - // We shift the render offset "inward", because drawRect with a stroke width greater - // than 1 draws outside the specified bounds. (We don't adjust the left edge, since - // we want to match the existing appearance of the "event square".) - r.left = x; - r.right = rightEdge - STROKE_WIDTH_ADJ; - r.top = y + STROKE_WIDTH_ADJ; - r.bottom = y + mEventHeight + BORDER_SPACE * 2 - STROKE_WIDTH_ADJ; - textX = x + BORDER_SPACE; - textY = y + mEventAscentHeight + BORDER_SPACE; - textRightEdge = rightEdge - BORDER_SPACE; - } else { - r.left = x; - r.right = x + EVENT_SQUARE_WIDTH; - r.bottom = y + mEventAscentHeight; - r.top = r.bottom - EVENT_SQUARE_WIDTH; - textX = x + EVENT_SQUARE_WIDTH + EVENT_RIGHT_PADDING; - textY = y + mEventAscentHeight; - textRightEdge = rightEdge; - } - - Style boxStyle = Style.STROKE; - boolean solidBackground = false; - if (event.selfAttendeeStatus != Attendees.ATTENDEE_STATUS_INVITED) { - boxStyle = Style.FILL_AND_STROKE; - if (allDay) { - solidBackground = true; - } - } - mEventSquarePaint.setStyle(boxStyle); - mEventSquarePaint.setColor(color); - canvas.drawRect(r, mEventSquarePaint); - - float avail = textRightEdge - textX; - CharSequence text = TextUtils.ellipsize( - event.title, mEventPaint, avail, TextUtils.TruncateAt.END); - Paint textPaint; - if (solidBackground) { - // Text color needs to contrast with solid background. - textPaint = mSolidBackgroundEventPaint; - } else if (isDeclined) { - // Use "declined event" color. - textPaint = mDeclinedEventPaint; - } else if (allDay) { - // Text inside frame is same color as frame. - mFramedEventPaint.setColor(color); - textPaint = mFramedEventPaint; - } else { - // Use generic event text color. - textPaint = mEventPaint; - } - canvas.drawText(text.toString(), textX, textY, textPaint); - y += mEventHeight; - if (allDay) { - y += BORDER_SPACE * 2; - } - - if (showTimes && !allDay) { - // show start/end time, e.g. "1pm - 2pm" - textY = y + mExtrasAscentHeight; - mStringBuilder.setLength(0); - text = DateUtils.formatDateRange(getContext(), mFormatter, event.startMillis, - event.endMillis, DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL, - Utils.getTimeZone(getContext(), null)).toString(); - text = TextUtils.ellipsize(text, mEventExtrasPaint, avail, TextUtils.TruncateAt.END); - canvas.drawText(text.toString(), textX, textY, isDeclined ? mEventDeclinedExtrasPaint - : mEventExtrasPaint); - y += mExtrasHeight; - } - - y += EVENT_LINE_PADDING; - - return y; - } - - protected void drawMoreEvents(Canvas canvas, int remainingEvents, int x) { - int y = mHeight - (mExtrasDescent + EVENT_BOTTOM_PADDING); - String text = getContext().getResources().getQuantityString( - R.plurals.month_more_events, remainingEvents); - mEventExtrasPaint.setAntiAlias(true); - mEventExtrasPaint.setFakeBoldText(true); - canvas.drawText(String.format(text, remainingEvents), x, y, mEventExtrasPaint); - mEventExtrasPaint.setFakeBoldText(false); - } - - /** - * Draws a line showing busy times in each day of week The method draws - * non-conflicting times in the event color and times with conflicting - * events in the dna conflict color defined in colors. - * - * @param canvas - */ - protected void drawDNA(Canvas canvas) { - // Draw event and conflict times - if (mDna != null) { - for (Utils.DNAStrand strand : mDna.values()) { - if (strand.color == CONFLICT_COLOR || strand.points == null - || strand.points.length == 0) { - continue; - } - mDNATimePaint.setColor(strand.color); - canvas.drawLines(strand.points, mDNATimePaint); - } - // Draw black last to make sure it's on top - Utils.DNAStrand strand = mDna.get(CONFLICT_COLOR); - if (strand != null && strand.points != null && strand.points.length != 0) { - mDNATimePaint.setColor(strand.color); - canvas.drawLines(strand.points, mDNATimePaint); - } - if (mDayXs == null) { - return; - } - int numDays = mDayXs.length; - int xOffset = (DNA_ALL_DAY_WIDTH - DNA_WIDTH) / 2; - if (strand != null && strand.allDays != null && strand.allDays.length == numDays) { - for (int i = 0; i < numDays; i++) { - // this adds at most 7 draws. We could sort it by color and - // build an array instead but this is easier. - if (strand.allDays[i] != 0) { - mDNAAllDayPaint.setColor(strand.allDays[i]); - canvas.drawLine(mDayXs[i] + xOffset, DNA_MARGIN, mDayXs[i] + xOffset, - DNA_MARGIN + DNA_ALL_DAY_HEIGHT, mDNAAllDayPaint); - } - } - } - } - } - - @Override - protected void updateSelectionPositions() { - if (mHasSelectedDay) { - int selectedPosition = mSelectedDay - mWeekStart; - if (selectedPosition < 0) { - selectedPosition += 7; - } - int effectiveWidth = mWidth - mPadding * 2; - effectiveWidth -= SPACING_WEEK_NUMBER; - mSelectedLeft = selectedPosition * effectiveWidth / mNumDays + mPadding; - mSelectedRight = (selectedPosition + 1) * effectiveWidth / mNumDays + mPadding; - mSelectedLeft += SPACING_WEEK_NUMBER; - mSelectedRight += SPACING_WEEK_NUMBER; - } - } - - public int getDayIndexFromLocation(float x) { - int dayStart = mShowWeekNum ? SPACING_WEEK_NUMBER + mPadding : mPadding; - if (x < dayStart || x > mWidth - mPadding) { - return -1; - } - // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels - return ((int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding))); - } - - @Override - public Time getDayFromLocation(float x) { - int dayPosition = getDayIndexFromLocation(x); - if (dayPosition == -1) { - return null; - } - int day = mFirstJulianDay + dayPosition; - - Time time = new Time(mTimeZone); - if (mWeek == 0) { - // This week is weird... - if (day < Time.EPOCH_JULIAN_DAY) { - day++; - } else if (day == Time.EPOCH_JULIAN_DAY) { - time.set(1, 0, 1970); - time.normalize(true); - return time; - } - } - - time.setJulianDay(day); - return time; - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - Context context = getContext(); - // only send accessibility events if accessibility and exploration are - // on. - AccessibilityManager am = (AccessibilityManager) context - .getSystemService(Service.ACCESSIBILITY_SERVICE); - if (!am.isEnabled() || !am.isTouchExplorationEnabled()) { - return super.onHoverEvent(event); - } - if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) { - Time hover = getDayFromLocation(event.getX()); - if (hover != null - && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) != 0)) { - Long millis = hover.toMillis(true); - String date = Utils.formatDateRange(context, millis, millis, - DateUtils.FORMAT_SHOW_DATE); - AccessibilityEvent accessEvent = AccessibilityEvent - .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); - accessEvent.getText().add(date); - if (mShowDetailsInMonth && mEvents != null) { - int dayStart = SPACING_WEEK_NUMBER + mPadding; - int dayPosition = (int) ((event.getX() - dayStart) * mNumDays / (mWidth - - dayStart - mPadding)); - ArrayList<Event> events = mEvents.get(dayPosition); - List<CharSequence> text = accessEvent.getText(); - for (Event e : events) { - text.add(e.getTitleAndLocation() + ". "); - int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; - if (!e.allDay) { - flags |= DateUtils.FORMAT_SHOW_TIME; - if (DateFormat.is24HourFormat(context)) { - flags |= DateUtils.FORMAT_24HOUR; - } - } else { - flags |= DateUtils.FORMAT_UTC; - } - text.add(Utils.formatDateRange(context, e.startMillis, e.endMillis, - flags) + ". "); - } - } - sendAccessibilityEventUnchecked(accessEvent); - mLastHoverTime = hover; - } - } - return true; - } - - public void setClickedDay(float xLocation) { - mClickedDayIndex = getDayIndexFromLocation(xLocation); - invalidate(); - } - public void clearClickedDay() { - mClickedDayIndex = -1; - invalidate(); - } -} diff --git a/src/com/android/calendar/month/MonthWeekEventsView.kt b/src/com/android/calendar/month/MonthWeekEventsView.kt new file mode 100644 index 00000000..e4b15494 --- /dev/null +++ b/src/com/android/calendar/month/MonthWeekEventsView.kt @@ -0,0 +1,1061 @@ +/* + * Copyright (C) 2021 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.calendar.month + +import com.android.calendar.Event +import com.android.calendar.R +import com.android.calendar.Utils +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator +import android.app.Service +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Paint.Align +import android.graphics.Paint.Style +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.provider.CalendarContract.Attendees +import android.text.TextPaint +import android.text.TextUtils +import android.text.format.DateFormat +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log +import android.view.MotionEvent +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityManager +import java.util.ArrayList +import java.util.Arrays +import java.util.Formatter +import java.util.HashMap +import java.util.Iterator +import java.util.List +import java.util.Locale + +class MonthWeekEventsView +/** + * Shows up as an error if we don't include this. + */ +(context: Context) : SimpleWeekView(context) { + // Renamed to avoid override modifier and type mismatch error + protected val mTodayTime: Time = Time() + override protected var mHasToday = false + protected var mTodayIndex = -1 + protected var mOrientation: Int = Configuration.ORIENTATION_LANDSCAPE + protected var mEvents: List<ArrayList<Event?>>? = null + protected var mUnsortedEvents: ArrayList<Event?>? = null + var mDna: HashMap<Int, Utils.DNAStrand>? = null + + // This is for drawing the outlines around event chips and supports up to 10 + // events being drawn on each day. The code will expand this if necessary. + protected var mEventOutlines: FloatRef = FloatRef(10 * 4 * 4 * 7) + protected var mMonthNamePaint: Paint? = null + protected var mEventPaint: TextPaint = TextPaint() + protected var mSolidBackgroundEventPaint: TextPaint? = null + protected var mFramedEventPaint: TextPaint? = null + protected var mDeclinedEventPaint: TextPaint? = null + protected var mEventExtrasPaint: TextPaint = TextPaint() + protected var mEventDeclinedExtrasPaint: TextPaint = TextPaint() + protected var mWeekNumPaint: Paint = Paint() + protected var mDNAAllDayPaint: Paint = Paint() + protected var mDNATimePaint: Paint = Paint() + protected var mEventSquarePaint: Paint = Paint() + protected var mTodayDrawable: Drawable? = null + protected var mMonthNumHeight = 0 + protected var mMonthNumAscentHeight = 0 + protected var mEventHeight = 0 + protected var mEventAscentHeight = 0 + protected var mExtrasHeight = 0 + protected var mExtrasAscentHeight = 0 + protected var mExtrasDescent = 0 + protected var mWeekNumAscentHeight = 0 + protected var mMonthBGColor = 0 + protected var mMonthBGOtherColor = 0 + protected var mMonthBGTodayColor = 0 + protected var mMonthNumColor = 0 + protected var mMonthNumOtherColor = 0 + protected var mMonthNumTodayColor = 0 + protected var mMonthNameColor = 0 + protected var mMonthNameOtherColor = 0 + protected var mMonthEventColor = 0 + protected var mMonthDeclinedEventColor = 0 + protected var mMonthDeclinedExtrasColor = 0 + protected var mMonthEventExtraColor = 0 + protected var mMonthEventOtherColor = 0 + protected var mMonthEventExtraOtherColor = 0 + protected var mMonthWeekNumColor = 0 + protected var mMonthBusyBitsBgColor = 0 + protected var mMonthBusyBitsBusyTimeColor = 0 + protected var mMonthBusyBitsConflictTimeColor = 0 + private var mClickedDayIndex = -1 + private var mClickedDayColor = 0 + protected var mEventChipOutlineColor = -0x1 + protected var mDaySeparatorInnerColor = 0 + protected var mTodayAnimateColor = 0 + private var mAnimateToday = false + private var mAnimateTodayAlpha = 0 + private var mTodayAnimator: ObjectAnimator? = null + private val mAnimatorListener: TodayAnimatorListener = TodayAnimatorListener() + + internal inner class TodayAnimatorListener : AnimatorListenerAdapter() { + @Volatile + private var mAnimator: Animator? = null + + @Volatile + private var mFadingIn = false + @Override + override fun onAnimationEnd(animation: Animator) { + synchronized(this) { + if (mAnimator !== animation) { + animation.removeAllListeners() + animation.cancel() + return + } + if (mFadingIn) { + if (mTodayAnimator != null) { + mTodayAnimator?.removeAllListeners() + mTodayAnimator?.cancel() + } + mTodayAnimator = ObjectAnimator.ofInt(this@MonthWeekEventsView, + "animateTodayAlpha", 255, 0) + mAnimator = mTodayAnimator + mFadingIn = false + mTodayAnimator?.addListener(this) + mTodayAnimator?.setDuration(600) + mTodayAnimator?.start() + } else { + mAnimateToday = false + mAnimateTodayAlpha = 0 + mAnimator?.removeAllListeners() + mAnimator = null + mTodayAnimator = null + invalidate() + } + } + } + + fun setAnimator(animation: Animator?) { + mAnimator = animation + } + + fun setFadingIn(fadingIn: Boolean) { + mFadingIn = fadingIn + } + } + + private var mDayXs: IntArray? = null + + /** + * This provides a reference to a float array which allows for easy size + * checking and reallocation. Used for drawing lines. + */ + inner class FloatRef(size: Int) { + var array: FloatArray + fun ensureSize(newSize: Int) { + if (newSize >= array.size) { + // Add enough space for 7 more boxes to be drawn + array = Arrays.copyOf(array, newSize + 16 * 7) + } + } + + init { + array = FloatArray(size) + } + } + + // Sets the list of events for this week. Takes a sorted list of arrays + // divided up by day for generating the large month version and the full + // arraylist sorted by start time to generate the dna version. + fun setEvents(sortedEvents: List<ArrayList<Event?>>?, unsortedEvents: ArrayList<Event?>?) { + setEvents(sortedEvents) + // The MIN_WEEK_WIDTH is a hack to prevent the view from trying to + // generate dna bits before its width has been fixed. + createDna(unsortedEvents) + } + + /** + * Sets up the dna bits for the view. This will return early if the view + * isn't in a state that will create a valid set of dna yet (such as the + * views width not being set correctly yet). + */ + fun createDna(unsortedEvents: ArrayList<Event?>?) { + if (unsortedEvents == null || mWidth <= MIN_WEEK_WIDTH || getContext() == null) { + // Stash the list of events for use when this view is ready, or + // just clear it if a null set has been passed to this view + mUnsortedEvents = unsortedEvents + mDna = null + return + } else { + // clear the cached set of events since we're ready to build it now + mUnsortedEvents = null + } + // Create the drawing coordinates for dna + if (!mShowDetailsInMonth) { + val numDays: Int = mEvents!!.size + var effectiveWidth: Int = mWidth - mPadding * 2 + if (mShowWeekNum) { + effectiveWidth -= SPACING_WEEK_NUMBER + } + DNA_ALL_DAY_WIDTH = effectiveWidth / numDays - 2 * DNA_SIDE_PADDING + mDNAAllDayPaint?.setStrokeWidth(DNA_ALL_DAY_WIDTH.toFloat()) + mDayXs = IntArray(numDays) + for (day in 0 until numDays) { + mDayXs!![day] = computeDayLeftPosition(day) + DNA_WIDTH / 2 + DNA_SIDE_PADDING + } + val top = DAY_SEPARATOR_INNER_WIDTH + DNA_MARGIN + DNA_ALL_DAY_HEIGHT + 1 + val bottom: Int = mHeight - DNA_MARGIN + mDna = Utils.createDNAStrands(mFirstJulianDay, unsortedEvents, top, bottom, + DNA_MIN_SEGMENT_HEIGHT, mDayXs, getContext()) + } + } + + fun setEvents(sortedEvents: List<ArrayList<Event?>>?) { + mEvents = sortedEvents + if (sortedEvents == null) { + return + } + if (sortedEvents.size !== mNumDays) { + if (Log.isLoggable(TAG, Log.ERROR)) { + Log.wtf(TAG, ("Events size must be same as days displayed: size=" + + sortedEvents.size) + " days=" + mNumDays) + } + mEvents = null + return + } + } + + protected fun loadColors(context: Context) { + val res: Resources = context.getResources() + mMonthWeekNumColor = res.getColor(R.color.month_week_num_color) + mMonthNumColor = res.getColor(R.color.month_day_number) + mMonthNumOtherColor = res.getColor(R.color.month_day_number_other) + mMonthNumTodayColor = res.getColor(R.color.month_today_number) + mMonthNameColor = mMonthNumColor + mMonthNameOtherColor = mMonthNumOtherColor + mMonthEventColor = res.getColor(R.color.month_event_color) + mMonthDeclinedEventColor = res.getColor(R.color.agenda_item_declined_color) + mMonthDeclinedExtrasColor = res.getColor(R.color.agenda_item_where_declined_text_color) + mMonthEventExtraColor = res.getColor(R.color.month_event_extra_color) + mMonthEventOtherColor = res.getColor(R.color.month_event_other_color) + mMonthEventExtraOtherColor = res.getColor(R.color.month_event_extra_other_color) + mMonthBGTodayColor = res.getColor(R.color.month_today_bgcolor) + mMonthBGOtherColor = res.getColor(R.color.month_other_bgcolor) + mMonthBGColor = res.getColor(R.color.month_bgcolor) + mDaySeparatorInnerColor = res.getColor(R.color.month_grid_lines) + mTodayAnimateColor = res.getColor(R.color.today_highlight_color) + mClickedDayColor = res.getColor(R.color.day_clicked_background_color) + mTodayDrawable = res.getDrawable(R.drawable.today_blue_week_holo_light) + } + + /** + * Sets up the text and style properties for painting. Override this if you + * want to use a different paint. + */ + @Override + protected override fun initView() { + super.initView() + if (!mInitialized) { + val resources: Resources = getContext().getResources() + mShowDetailsInMonth = Utils.getConfigBool(getContext(), R.bool.show_details_in_month) + TEXT_SIZE_EVENT_TITLE = resources.getInteger(R.integer.text_size_event_title) + TEXT_SIZE_MONTH_NUMBER = resources.getInteger(R.integer.text_size_month_number) + SIDE_PADDING_MONTH_NUMBER = resources.getInteger(R.integer.month_day_number_margin) + CONFLICT_COLOR = resources.getColor(R.color.month_dna_conflict_time_color) + EVENT_TEXT_COLOR = resources.getColor(R.color.calendar_event_text_color) + if (mScale != 1f) { + TOP_PADDING_MONTH_NUMBER *= mScale.toInt() + TOP_PADDING_WEEK_NUMBER *= mScale.toInt() + SIDE_PADDING_MONTH_NUMBER *= mScale.toInt() + SIDE_PADDING_WEEK_NUMBER *= mScale.toInt() + SPACING_WEEK_NUMBER *= mScale.toInt() + TEXT_SIZE_MONTH_NUMBER *= mScale.toInt() + TEXT_SIZE_EVENT *= mScale.toInt() + TEXT_SIZE_EVENT_TITLE *= mScale.toInt() + TEXT_SIZE_MORE_EVENTS *= mScale.toInt() + TEXT_SIZE_MONTH_NAME *= mScale.toInt() + TEXT_SIZE_WEEK_NUM *= mScale.toInt() + DAY_SEPARATOR_OUTER_WIDTH *= mScale.toInt() + DAY_SEPARATOR_INNER_WIDTH *= mScale.toInt() + DAY_SEPARATOR_VERTICAL_LENGTH *= mScale.toInt() + DAY_SEPARATOR_VERTICAL_LENGTH_PORTRAIT *= mScale.toInt() + EVENT_X_OFFSET_LANDSCAPE *= mScale.toInt() + EVENT_Y_OFFSET_LANDSCAPE *= mScale.toInt() + EVENT_Y_OFFSET_PORTRAIT *= mScale.toInt() + EVENT_SQUARE_WIDTH *= mScale.toInt() + EVENT_SQUARE_BORDER *= mScale.toInt() + EVENT_LINE_PADDING *= mScale.toInt() + EVENT_BOTTOM_PADDING *= mScale.toInt() + EVENT_RIGHT_PADDING *= mScale.toInt() + DNA_MARGIN *= mScale.toInt() + DNA_WIDTH *= mScale.toInt() + DNA_ALL_DAY_HEIGHT *= mScale.toInt() + DNA_MIN_SEGMENT_HEIGHT *= mScale.toInt() + DNA_SIDE_PADDING *= mScale.toInt() + DEFAULT_EDGE_SPACING *= mScale.toInt() + DNA_ALL_DAY_WIDTH *= mScale.toInt() + TODAY_HIGHLIGHT_WIDTH *= mScale.toInt() + } + if (!mShowDetailsInMonth) { + TOP_PADDING_MONTH_NUMBER += DNA_ALL_DAY_HEIGHT + DNA_MARGIN + } + mInitialized = true + } + mPadding = DEFAULT_EDGE_SPACING + loadColors(getContext()) + // TODO modify paint properties depending on isMini + mMonthNumPaint = Paint() + mMonthNumPaint?.setFakeBoldText(false) + mMonthNumPaint?.setAntiAlias(true) + mMonthNumPaint?.setTextSize(TEXT_SIZE_MONTH_NUMBER.toFloat()) + mMonthNumPaint?.setColor(mMonthNumColor) + mMonthNumPaint?.setStyle(Style.FILL) + mMonthNumPaint?.setTextAlign(Align.RIGHT) + mMonthNumPaint?.setTypeface(Typeface.DEFAULT) + mMonthNumAscentHeight = (-mMonthNumPaint!!.ascent() + 0.5f).toInt() + mMonthNumHeight = (mMonthNumPaint!!.descent() - mMonthNumPaint!!.ascent() + 0.5f).toInt() + mEventPaint = TextPaint() + mEventPaint?.setFakeBoldText(true) + mEventPaint?.setAntiAlias(true) + mEventPaint?.setTextSize(TEXT_SIZE_EVENT_TITLE.toFloat()) + mEventPaint?.setColor(mMonthEventColor) + mSolidBackgroundEventPaint = TextPaint(mEventPaint) + mSolidBackgroundEventPaint?.setColor(EVENT_TEXT_COLOR) + mFramedEventPaint = TextPaint(mSolidBackgroundEventPaint) + mDeclinedEventPaint = TextPaint() + mDeclinedEventPaint?.setFakeBoldText(true) + mDeclinedEventPaint?.setAntiAlias(true) + mDeclinedEventPaint?.setTextSize(TEXT_SIZE_EVENT_TITLE.toFloat()) + mDeclinedEventPaint?.setColor(mMonthDeclinedEventColor) + mEventAscentHeight = (-mEventPaint.ascent() + 0.5f).toInt() + mEventHeight = (mEventPaint.descent() - mEventPaint.ascent() + 0.5f).toInt() + mEventExtrasPaint = TextPaint() + mEventExtrasPaint?.setFakeBoldText(false) + mEventExtrasPaint?.setAntiAlias(true) + mEventExtrasPaint?.setStrokeWidth(EVENT_SQUARE_BORDER.toFloat()) + mEventExtrasPaint?.setTextSize(TEXT_SIZE_EVENT.toFloat()) + mEventExtrasPaint?.setColor(mMonthEventExtraColor) + mEventExtrasPaint?.setStyle(Style.FILL) + mEventExtrasPaint?.setTextAlign(Align.LEFT) + mExtrasHeight = (mEventExtrasPaint.descent() - mEventExtrasPaint.ascent() + 0.5f).toInt() + mExtrasAscentHeight = (-mEventExtrasPaint.ascent() + 0.5f).toInt() + mExtrasDescent = (mEventExtrasPaint.descent() + 0.5f).toInt() + mEventDeclinedExtrasPaint = TextPaint() + mEventDeclinedExtrasPaint.setFakeBoldText(false) + mEventDeclinedExtrasPaint.setAntiAlias(true) + mEventDeclinedExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER.toFloat()) + mEventDeclinedExtrasPaint.setTextSize(TEXT_SIZE_EVENT.toFloat()) + mEventDeclinedExtrasPaint.setColor(mMonthDeclinedExtrasColor) + mEventDeclinedExtrasPaint.setStyle(Style.FILL) + mEventDeclinedExtrasPaint.setTextAlign(Align.LEFT) + mWeekNumPaint = Paint() + mWeekNumPaint.setFakeBoldText(false) + mWeekNumPaint.setAntiAlias(true) + mWeekNumPaint.setTextSize(TEXT_SIZE_WEEK_NUM.toFloat()) + mWeekNumPaint.setColor(mWeekNumColor) + mWeekNumPaint.setStyle(Style.FILL) + mWeekNumPaint.setTextAlign(Align.RIGHT) + mWeekNumAscentHeight = (-mWeekNumPaint.ascent() + 0.5f).toInt() + mDNAAllDayPaint = Paint() + mDNATimePaint = Paint() + mDNATimePaint.setColor(mMonthBusyBitsBusyTimeColor) + mDNATimePaint.setStyle(Style.FILL_AND_STROKE) + mDNATimePaint.setStrokeWidth(DNA_WIDTH.toFloat()) + mDNATimePaint.setAntiAlias(false) + mDNAAllDayPaint.setColor(mMonthBusyBitsConflictTimeColor) + mDNAAllDayPaint.setStyle(Style.FILL_AND_STROKE) + mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH.toFloat()) + mDNAAllDayPaint.setAntiAlias(false) + mEventSquarePaint = Paint() + mEventSquarePaint.setStrokeWidth(EVENT_SQUARE_BORDER.toFloat()) + mEventSquarePaint.setAntiAlias(false) + if (DEBUG_LAYOUT) { + Log.d("EXTRA", "mScale=$mScale") + Log.d("EXTRA", "mMonthNumPaint ascent=" + mMonthNumPaint?.ascent() + ?.toString() + " descent=" + mMonthNumPaint?.descent()?.toString() + + " int height=" + mMonthNumHeight) + Log.d("EXTRA", "mEventPaint ascent=" + mEventPaint?.ascent() + ?.toString() + " descent=" + mEventPaint.descent().toString() + + " int height=" + mEventHeight + .toString() + " int ascent=" + mEventAscentHeight) + Log.d("EXTRA", "mEventExtrasPaint ascent=" + mEventExtrasPaint.ascent() + .toString() + " descent=" + mEventExtrasPaint.descent().toString() + + " int height=" + mExtrasHeight) + Log.d("EXTRA", "mWeekNumPaint ascent=" + mWeekNumPaint.ascent() + .toString() + " descent=" + mWeekNumPaint.descent()) + } + } + + @Override + override fun setWeekParams(params: HashMap<String?, Int?>, tz: String) { + super.setWeekParams(params, tz) + if (params.containsKey(VIEW_PARAMS_ORIENTATION)) { + mOrientation = params.get(VIEW_PARAMS_ORIENTATION) ?: + Configuration.ORIENTATION_LANDSCAPE + } + updateToday(tz) + mNumCells = mNumDays + 1 + if (params.containsKey(VIEW_PARAMS_ANIMATE_TODAY) && mHasToday) { + synchronized(mAnimatorListener) { + if (mTodayAnimator != null) { + mTodayAnimator?.removeAllListeners() + mTodayAnimator?.cancel() + } + mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha", + Math.max(mAnimateTodayAlpha, 80), 255) + mTodayAnimator?.setDuration(150) + mAnimatorListener.setAnimator(mTodayAnimator) + mAnimatorListener.setFadingIn(true) + mTodayAnimator?.addListener(mAnimatorListener) + mAnimateToday = true + mTodayAnimator?.start() + } + } + } + + /** + * @param tz + */ + fun updateToday(tz: String): Boolean { + mTodayTime.timezone = tz + mTodayTime.setToNow() + mTodayTime.normalize(true) + val julianToday: Int = Time.getJulianDay(mTodayTime.toMillis(false), mTodayTime.gmtoff) + if (julianToday >= mFirstJulianDay && julianToday < mFirstJulianDay + mNumDays) { + mHasToday = true + mTodayIndex = julianToday - mFirstJulianDay + } else { + mHasToday = false + mTodayIndex = -1 + } + return mHasToday + } + + fun setAnimateTodayAlpha(alpha: Int) { + mAnimateTodayAlpha = alpha + invalidate() + } + + @Override + protected override fun onDraw(canvas: Canvas) { + drawBackground(canvas) + drawWeekNums(canvas) + drawDaySeparators(canvas) + if (mHasToday && mAnimateToday) { + drawToday(canvas) + } + if (mShowDetailsInMonth) { + drawEvents(canvas) + } else { + if (mDna == null && mUnsortedEvents != null) { + createDna(mUnsortedEvents) + } + drawDNA(canvas) + } + drawClick(canvas) + } + + protected fun drawToday(canvas: Canvas) { + r.top = DAY_SEPARATOR_INNER_WIDTH + TODAY_HIGHLIGHT_WIDTH / 2 + r.bottom = mHeight - Math.ceil(TODAY_HIGHLIGHT_WIDTH.toDouble() / 2.0f).toInt() + p.setStyle(Style.STROKE) + p.setStrokeWidth(TODAY_HIGHLIGHT_WIDTH.toFloat()) + r.left = computeDayLeftPosition(mTodayIndex) + TODAY_HIGHLIGHT_WIDTH / 2 + r.right = (computeDayLeftPosition(mTodayIndex + 1) + - Math.ceil(TODAY_HIGHLIGHT_WIDTH.toDouble() / 2.0f).toInt()) + p.setColor(mTodayAnimateColor or (mAnimateTodayAlpha shl 24)) + canvas.drawRect(r, p) + p.setStyle(Style.FILL) + } + + // TODO move into SimpleWeekView + // Computes the x position for the left side of the given day + private fun computeDayLeftPosition(day: Int): Int { + var effectiveWidth: Int = mWidth + var x = 0 + var xOffset = 0 + if (mShowWeekNum) { + xOffset = SPACING_WEEK_NUMBER + mPadding + effectiveWidth -= xOffset + } + x = day * effectiveWidth / mNumDays + xOffset + return x + } + + @Override + protected override fun drawDaySeparators(canvas: Canvas) { + val lines = FloatArray(8 * 4) + var count = 6 * 4 + var wkNumOffset = 0 + var i = 0 + if (mShowWeekNum) { + // This adds the first line separating the week number + val xOffset: Int = SPACING_WEEK_NUMBER + mPadding + count += 4 + lines[i++] = xOffset.toFloat() + lines[i++] = 0f + lines[i++] = xOffset.toFloat() + lines[i++] = mHeight.toFloat() + wkNumOffset++ + } + count += 4 + lines[i++] = 0f + lines[i++] = 0f + lines[i++] = mWidth.toFloat() + lines[i++] = 0f + val y0 = 0 + val y1: Int = mHeight + while (i < count) { + val x = computeDayLeftPosition(i / 4 - wkNumOffset) + lines[i++] = x.toFloat() + lines[i++] = y0.toFloat() + lines[i++] = x.toFloat() + lines[i++] = y1.toFloat() + } + p.setColor(mDaySeparatorInnerColor) + p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH.toFloat()) + canvas.drawLines(lines, 0, count, p) + } + + @Override + protected override fun drawBackground(canvas: Canvas) { + var i = 0 + var offset = 0 + r.top = DAY_SEPARATOR_INNER_WIDTH + r.bottom = mHeight + if (mShowWeekNum) { + i++ + offset++ + } + if (!mOddMonth!!.get(i)) { + while (++i < mOddMonth!!.size && !mOddMonth!!.get(i)); + r.right = computeDayLeftPosition(i - offset) + r.left = 0 + p.setColor(mMonthBGOtherColor) + canvas.drawRect(r, p) + // compute left edge for i, set up r, draw + } else if (!mOddMonth!!.get(mOddMonth!!.size - 1.also { i = it })) { + while (--i >= offset && !mOddMonth!!.get(i)); + i++ + // compute left edge for i, set up r, draw + r.right = mWidth + r.left = computeDayLeftPosition(i - offset) + p.setColor(mMonthBGOtherColor) + canvas.drawRect(r, p) + } + if (mHasToday) { + p.setColor(mMonthBGTodayColor) + r.left = computeDayLeftPosition(mTodayIndex) + r.right = computeDayLeftPosition(mTodayIndex + 1) + canvas.drawRect(r, p) + } + } + + // Draw the "clicked" color on the tapped day + private fun drawClick(canvas: Canvas) { + if (mClickedDayIndex != -1) { + val alpha: Int = p.getAlpha() + p.setColor(mClickedDayColor) + p.setAlpha(mClickedAlpha) + r.left = computeDayLeftPosition(mClickedDayIndex) + r.right = computeDayLeftPosition(mClickedDayIndex + 1) + r.top = DAY_SEPARATOR_INNER_WIDTH + r.bottom = mHeight + canvas.drawRect(r, p) + p.setAlpha(alpha) + } + } + + @Override + protected override fun drawWeekNums(canvas: Canvas) { + var y: Int + var i = 0 + var offset = -1 + var todayIndex = mTodayIndex + var x = 0 + var numCount: Int = mNumDays + if (mShowWeekNum) { + x = SIDE_PADDING_WEEK_NUMBER + mPadding + y = mWeekNumAscentHeight + TOP_PADDING_WEEK_NUMBER + canvas.drawText(mDayNumbers!!.get(0) as String, x.toFloat(), y.toFloat(), mWeekNumPaint) + numCount++ + i++ + todayIndex++ + offset++ + } + y = mMonthNumAscentHeight + TOP_PADDING_MONTH_NUMBER + var isFocusMonth: Boolean = mFocusDay!!.get(i) + var isBold = false + mMonthNumPaint?.setColor(if (isFocusMonth) mMonthNumColor else mMonthNumOtherColor) + while (i < numCount) { + if (mHasToday && todayIndex == i) { + mMonthNumPaint?.setColor(mMonthNumTodayColor) + mMonthNumPaint?.setFakeBoldText(true.also { isBold = it }) + if (i + 1 < numCount) { + // Make sure the color will be set back on the next + // iteration + isFocusMonth = !mFocusDay!!.get(i + 1) + } + } else if (mFocusDay?.get(i) !== isFocusMonth) { + isFocusMonth = mFocusDay!!.get(i) + mMonthNumPaint?.setColor(if (isFocusMonth) mMonthNumColor else mMonthNumOtherColor) + } + x = computeDayLeftPosition(i - offset) - SIDE_PADDING_MONTH_NUMBER + canvas.drawText(mDayNumbers!!.get(i) as String, x.toFloat(), y.toFloat(), + mMonthNumPaint as Paint) + if (isBold) { + mMonthNumPaint?.setFakeBoldText(false.also { isBold = it }) + } + i++ + } + } + + protected fun drawEvents(canvas: Canvas) { + if (mEvents == null) { + return + } + var day = -1 + for (eventDay in mEvents!!) { + day++ + if (eventDay == null || eventDay.size === 0) { + continue + } + var ySquare: Int + val xSquare = computeDayLeftPosition(day) + SIDE_PADDING_MONTH_NUMBER + 1 + var rightEdge = computeDayLeftPosition(day + 1) + if (mOrientation == Configuration.ORIENTATION_PORTRAIT) { + ySquare = EVENT_Y_OFFSET_PORTRAIT + mMonthNumHeight + TOP_PADDING_MONTH_NUMBER + rightEdge -= SIDE_PADDING_MONTH_NUMBER + 1 + } else { + ySquare = EVENT_Y_OFFSET_LANDSCAPE + rightEdge -= EVENT_X_OFFSET_LANDSCAPE + } + + // Determine if everything will fit when time ranges are shown. + var showTimes = true + var iter: Iterator<Event> = eventDay.iterator() as Iterator<Event> + var yTest = ySquare + while (iter.hasNext()) { + val event: Event = iter.next() + val newY = drawEvent(canvas, event, xSquare, yTest, rightEdge, iter.hasNext(), + showTimes, /*doDraw*/false) + if (newY == yTest) { + showTimes = false + break + } + yTest = newY + } + var eventCount = 0 + iter = eventDay.iterator() as Iterator<Event> + while (iter.hasNext()) { + val event: Event = iter.next() + val newY = drawEvent(canvas, event, xSquare, ySquare, rightEdge, iter.hasNext(), + showTimes, /*doDraw*/true) + if (newY == ySquare) { + break + } + eventCount++ + ySquare = newY + } + val remaining: Int = eventDay.size- eventCount + if (remaining > 0) { + drawMoreEvents(canvas, remaining, xSquare) + } + } + } + + protected fun addChipOutline(lines: FloatRef, count: Int, x: Int, y: Int): Int { + var count = count + lines.ensureSize(count + 16) + // top of box + lines.array[count++] = x.toFloat() + lines.array[count++] = y.toFloat() + lines.array[count++] = (x + EVENT_SQUARE_WIDTH).toFloat() + lines.array[count++] = y.toFloat() + // right side of box + lines.array[count++] = (x + EVENT_SQUARE_WIDTH).toFloat() + lines.array[count++] = y.toFloat() + lines.array[count++] = (x + EVENT_SQUARE_WIDTH).toFloat() + lines.array[count++] = (y + EVENT_SQUARE_WIDTH).toFloat() + // left side of box + lines.array[count++] = x.toFloat() + lines.array[count++] = y.toFloat() + lines.array[count++] = x.toFloat() + lines.array[count++] = (y + EVENT_SQUARE_WIDTH + 1).toFloat() + // bottom of box + lines.array[count++] = x.toFloat() + lines.array[count++] = (y + EVENT_SQUARE_WIDTH).toFloat() + lines.array[count++] = (x + EVENT_SQUARE_WIDTH + 1).toFloat() + lines.array[count++] = (y + EVENT_SQUARE_WIDTH).toFloat() + return count + } + + /** + * Attempts to draw the given event. Returns the y for the next event or the + * original y if the event will not fit. An event is considered to not fit + * if the event and its extras won't fit or if there are more events and the + * more events line would not fit after drawing this event. + * + * @param canvas the canvas to draw on + * @param event the event to draw + * @param x the top left corner for this event's color chip + * @param y the top left corner for this event's color chip + * @param rightEdge the rightmost point we're allowed to draw on (exclusive) + * @param moreEvents indicates whether additional events will follow this one + * @param showTimes if set, a second line with a time range will be displayed for non-all-day + * events + * @param doDraw if set, do the actual drawing; otherwise this just computes the height + * and returns + * @return the y for the next event or the original y if it won't fit + */ + protected fun drawEvent(canvas: Canvas, event: Event, x: Int, y: Int, rightEdge: Int, + moreEvents: Boolean, showTimes: Boolean, doDraw: Boolean): Int { + /* + * Vertical layout: + * (top of box) + * a. EVENT_Y_OFFSET_LANDSCAPE or portrait equivalent + * b. Event title: mEventHeight for a normal event, + 2xBORDER_SPACE for all-day event + * c. [optional] Time range (mExtrasHeight) + * d. EVENT_LINE_PADDING + * + * Repeat (b,c,d) as needed and space allows. If we have more events than fit, we need + * to leave room for something like "+2" at the bottom: + * + * e. "+ more" line (mExtrasHeight) + * + * f. EVENT_BOTTOM_PADDING (overlaps EVENT_LINE_PADDING) + * (bottom of box) + */ + var y = y + val BORDER_SPACE = EVENT_SQUARE_BORDER + 1 // want a 1-pixel gap inside border + val STROKE_WIDTH_ADJ = EVENT_SQUARE_BORDER / 2 // adjust bounds for stroke width + val allDay: Boolean = event.allDay + var eventRequiredSpace = mEventHeight + if (allDay) { + // Add a few pixels for the box we draw around all-day events. + eventRequiredSpace += BORDER_SPACE * 2 + } else if (showTimes) { + // Need room for the "1pm - 2pm" line. + eventRequiredSpace += mExtrasHeight + } + var reservedSpace = EVENT_BOTTOM_PADDING // leave a bit of room at the bottom + if (moreEvents) { + // More events follow. Leave a bit of space between events. + eventRequiredSpace += EVENT_LINE_PADDING + + // Make sure we have room for the "+ more" line. (The "+ more" line is expected + // to be <= the height of an event line, so we won't show "+1" when we could be + // showing the event.) + reservedSpace += mExtrasHeight + } + if (y + eventRequiredSpace + reservedSpace > mHeight) { + // Not enough space, return original y + return y + } else if (!doDraw) { + return y + eventRequiredSpace + } + val isDeclined = event.selfAttendeeStatus === Attendees.ATTENDEE_STATUS_DECLINED + var color: Int = event.color + if (isDeclined) { + color = Utils.getDeclinedColorFromColor(color) + } + val textX: Int + var textY: Int + val textRightEdge: Int + if (allDay) { + // We shift the render offset "inward", because drawRect with a stroke width greater + // than 1 draws outside the specified bounds. (We don't adjust the left edge, since + // we want to match the existing appearance of the "event square".) + r.left = x + r.right = rightEdge - STROKE_WIDTH_ADJ + r.top = y + STROKE_WIDTH_ADJ + r.bottom = y + mEventHeight + BORDER_SPACE * 2 - STROKE_WIDTH_ADJ + textX = x + BORDER_SPACE + textY = y + mEventAscentHeight + BORDER_SPACE + textRightEdge = rightEdge - BORDER_SPACE + } else { + r.left = x + r.right = x + EVENT_SQUARE_WIDTH + r.bottom = y + mEventAscentHeight + r.top = r.bottom - EVENT_SQUARE_WIDTH + textX = x + EVENT_SQUARE_WIDTH + EVENT_RIGHT_PADDING + textY = y + mEventAscentHeight + textRightEdge = rightEdge + } + var boxStyle: Style = Style.STROKE + var solidBackground = false + if (event.selfAttendeeStatus !== Attendees.ATTENDEE_STATUS_INVITED) { + boxStyle = Style.FILL_AND_STROKE + if (allDay) { + solidBackground = true + } + } + mEventSquarePaint.setStyle(boxStyle) + mEventSquarePaint.setColor(color) + canvas.drawRect(r, mEventSquarePaint) + val avail = (textRightEdge - textX).toFloat() + var text: CharSequence = TextUtils.ellipsize( + event.title, mEventPaint, avail, TextUtils.TruncateAt.END) + val textPaint: TextPaint? + textPaint = if (solidBackground) { + // Text color needs to contrast with solid background. + mSolidBackgroundEventPaint + } else if (isDeclined) { + // Use "declined event" color. + mDeclinedEventPaint + } else if (allDay) { + // Text inside frame is same color as frame. + mFramedEventPaint?.setColor(color) + mFramedEventPaint + } else { + // Use generic event text color. + mEventPaint + } + canvas.drawText(text.toString(), textX.toFloat(), textY.toFloat(), textPaint as Paint) + y += mEventHeight + if (allDay) { + y += BORDER_SPACE * 2 + } + if (showTimes && !allDay) { + // show start/end time, e.g. "1pm - 2pm" + textY = y + mExtrasAscentHeight + mStringBuilder.setLength(0) + text = DateUtils.formatDateRange(getContext(), mFormatter, event.startMillis, + event.endMillis, DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL, + Utils.getTimeZone(getContext(), null)).toString() + text = TextUtils.ellipsize(text, mEventExtrasPaint, avail, TextUtils.TruncateAt.END) + canvas.drawText(text.toString(), textX.toFloat(), textY.toFloat(), + if (isDeclined) mEventDeclinedExtrasPaint else mEventExtrasPaint) + y += mExtrasHeight + } + y += EVENT_LINE_PADDING + return y + } + + protected fun drawMoreEvents(canvas: Canvas, remainingEvents: Int, x: Int) { + val y: Int = mHeight - (mExtrasDescent + EVENT_BOTTOM_PADDING) + val text: String = getContext().getResources().getQuantityString( + R.plurals.month_more_events, remainingEvents) + mEventExtrasPaint.setAntiAlias(true) + mEventExtrasPaint.setFakeBoldText(true) + canvas.drawText(String.format(text, remainingEvents), x.toFloat(), y.toFloat(), + mEventExtrasPaint as Paint) + mEventExtrasPaint!!.setFakeBoldText(false) + } + + /** + * Draws a line showing busy times in each day of week The method draws + * non-conflicting times in the event color and times with conflicting + * events in the dna conflict color defined in colors. + * + * @param canvas + */ + protected fun drawDNA(canvas: Canvas) { + // Draw event and conflict times + if (mDna != null) { + for (strand in mDna!!.values) { + if (strand.color === CONFLICT_COLOR || strand.points == null || + (strand.points as FloatArray).size === 0) { + continue + } + mDNATimePaint!!.setColor(strand.color) + canvas.drawLines(strand.points as FloatArray, mDNATimePaint as Paint) + } + // Draw black last to make sure it's on top + val strand: Utils.DNAStrand? = mDna?.get(CONFLICT_COLOR) + if (strand != null && strand!!.points != null && strand!!.points?.size !== 0) { + mDNATimePaint!!.setColor(strand.color) + canvas.drawLines(strand.points as FloatArray, mDNATimePaint as Paint) + } + if (mDayXs == null) { + return + } + val numDays = mDayXs!!.size + val xOffset = (DNA_ALL_DAY_WIDTH - DNA_WIDTH) / 2 + if (strand != null && strand!!.allDays != null && strand!!.allDays?.size === numDays) { + for (i in 0 until numDays) { + // this adds at most 7 draws. We could sort it by color and + // build an array instead but this is easier. + if (strand!!.allDays?.get(i) !== 0) { + mDNAAllDayPaint!!.setColor(strand!!.allDays!!.get(i)) + canvas.drawLine(mDayXs!![i].toFloat() + xOffset.toFloat(), + DNA_MARGIN.toFloat(), mDayXs!![i].toFloat() + xOffset.toFloat(), + DNA_MARGIN.toFloat() + DNA_ALL_DAY_HEIGHT.toFloat(), + mDNAAllDayPaint as Paint) + } + } + } + } + } + + @Override + protected override fun updateSelectionPositions() { + if (mHasSelectedDay) { + var selectedPosition: Int = mSelectedDay - mWeekStart + if (selectedPosition < 0) { + selectedPosition += 7 + } + var effectiveWidth: Int = mWidth - mPadding * 2 + effectiveWidth -= SPACING_WEEK_NUMBER + mSelectedLeft = selectedPosition * effectiveWidth / mNumDays + mPadding + mSelectedRight = (selectedPosition + 1) * effectiveWidth / mNumDays + mPadding + mSelectedLeft += SPACING_WEEK_NUMBER + mSelectedRight += SPACING_WEEK_NUMBER + } + } + + fun getDayIndexFromLocation(x: Float): Int { + val dayStart: Int = if (mShowWeekNum) SPACING_WEEK_NUMBER + mPadding else mPadding + return if (x < dayStart || x > mWidth - mPadding) { + -1 + } else (((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)).toInt()) + // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels + } + + @Override + override fun getDayFromLocation(x: Float): Time? { + val dayPosition = getDayIndexFromLocation(x) + if (dayPosition == -1) { + return null + } + var day: Int = mFirstJulianDay + dayPosition + val time = Time(mTimeZone) + if (mWeek === 0) { + // This week is weird... + if (day < Time.EPOCH_JULIAN_DAY) { + day++ + } else if (day == Time.EPOCH_JULIAN_DAY) { + time.set(1, 0, 1970) + time.normalize(true) + return time + } + } + time.setJulianDay(day) + return time + } + + @Override + override fun onHoverEvent(event: MotionEvent): Boolean { + val context: Context = getContext() + // only send accessibility events if accessibility and exploration are + // on. + val am: AccessibilityManager = context + .getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager + if (!am.isEnabled() || !am.isTouchExplorationEnabled()) { + return super.onHoverEvent(event) + } + if (event.getAction() !== MotionEvent.ACTION_HOVER_EXIT) { + val hover: Time? = getDayFromLocation(event.getX()) + if (hover != null + && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) !== 0)) { + val millis: Long = hover.toMillis(true) + val date: String = Utils!!.formatDateRange(context, millis, millis, + DateUtils.FORMAT_SHOW_DATE) as String + val accessEvent: AccessibilityEvent = AccessibilityEvent + .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) + accessEvent.getText().add(date) + if (mShowDetailsInMonth && mEvents != null) { + val dayStart: Int = SPACING_WEEK_NUMBER + mPadding + val dayPosition = ((event.getX() - dayStart) * mNumDays / (mWidth + - dayStart - mPadding)).toInt() + val events: ArrayList<Event?> = mEvents!![dayPosition] + val text: List<CharSequence> = accessEvent.getText() as List<CharSequence> + for (e in events) { + text.add(e!!.titleAndLocation.toString() + ". ") + var flags: Int = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR + if (!e!!.allDay) { + flags = flags or DateUtils.FORMAT_SHOW_TIME + if (DateFormat.is24HourFormat(context)) { + flags = flags or DateUtils.FORMAT_24HOUR + } + } else { + flags = flags or DateUtils.FORMAT_UTC + } + text.add(Utils.formatDateRange(context, e!!.startMillis, e!!.endMillis, + flags).toString() + ". ") + } + } + sendAccessibilityEventUnchecked(accessEvent) + mLastHoverTime = hover + } + } + return true + } + + fun setClickedDay(xLocation: Float) { + mClickedDayIndex = getDayIndexFromLocation(xLocation) + invalidate() + } + + fun clearClickedDay() { + mClickedDayIndex = -1 + invalidate() + } + + companion object { + private const val TAG = "MonthView" + private const val DEBUG_LAYOUT = false + const val VIEW_PARAMS_ORIENTATION = "orientation" + const val VIEW_PARAMS_ANIMATE_TODAY = "animate_today" + + /* NOTE: these are not constants, and may be multiplied by a scale factor */ + private var TEXT_SIZE_MONTH_NUMBER = 32 + private var TEXT_SIZE_EVENT = 12 + private var TEXT_SIZE_EVENT_TITLE = 14 + private var TEXT_SIZE_MORE_EVENTS = 12 + private var TEXT_SIZE_MONTH_NAME = 14 + private var TEXT_SIZE_WEEK_NUM = 12 + private var DNA_MARGIN = 4 + private var DNA_ALL_DAY_HEIGHT = 4 + private var DNA_MIN_SEGMENT_HEIGHT = 4 + private var DNA_WIDTH = 8 + private var DNA_ALL_DAY_WIDTH = 32 + private var DNA_SIDE_PADDING = 6 + private var CONFLICT_COLOR: Int = Color.BLACK + private var EVENT_TEXT_COLOR: Int = Color.WHITE + private var DEFAULT_EDGE_SPACING = 0 + private var SIDE_PADDING_MONTH_NUMBER = 4 + private var TOP_PADDING_MONTH_NUMBER = 4 + private var TOP_PADDING_WEEK_NUMBER = 4 + private var SIDE_PADDING_WEEK_NUMBER = 20 + private var DAY_SEPARATOR_OUTER_WIDTH = 0 + private var DAY_SEPARATOR_INNER_WIDTH = 1 + private var DAY_SEPARATOR_VERTICAL_LENGTH = 53 + private var DAY_SEPARATOR_VERTICAL_LENGTH_PORTRAIT = 64 + private const val MIN_WEEK_WIDTH = 50 + private var EVENT_X_OFFSET_LANDSCAPE = 38 + private var EVENT_Y_OFFSET_LANDSCAPE = 8 + private var EVENT_Y_OFFSET_PORTRAIT = 7 + private var EVENT_SQUARE_WIDTH = 10 + private var EVENT_SQUARE_BORDER = 2 + private var EVENT_LINE_PADDING = 2 + private var EVENT_RIGHT_PADDING = 4 + private var EVENT_BOTTOM_PADDING = 3 + private var TODAY_HIGHLIGHT_WIDTH = 2 + private var SPACING_WEEK_NUMBER = 24 + private var mInitialized = false + private var mShowDetailsInMonth = false + protected var mStringBuilder: StringBuilder = StringBuilder(50) + + // TODO recreate formatter when locale changes + protected var mFormatter: Formatter = Formatter(mStringBuilder, Locale.getDefault()) + private const val mClickedAlpha = 128 + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/month/SimpleDayPickerFragment.java b/src/com/android/calendar/month/SimpleDayPickerFragment.java deleted file mode 100644 index 2efae6a9..00000000 --- a/src/com/android/calendar/month/SimpleDayPickerFragment.java +++ /dev/null @@ -1,612 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar.month; - -import com.android.calendar.R; -import com.android.calendar.Utils; - -import android.app.Activity; -import android.app.ListFragment; -import android.content.Context; -import android.content.res.Resources; -import android.database.DataSetObserver; -import android.os.Bundle; -import android.os.Handler; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.ListView; -import android.widget.TextView; - -import java.util.Calendar; -import java.util.HashMap; -import java.util.Locale; - -/** - * <p> - * This displays a titled list of weeks with selectable days. It can be - * configured to display the week number, start the week on a given day, show a - * reduced number of days, or display an arbitrary number of weeks at a time. By - * overriding methods and changing variables this fragment can be customized to - * easily display a month selection component in a given style. - * </p> - */ -public class SimpleDayPickerFragment extends ListFragment implements OnScrollListener { - - private static final String TAG = "MonthFragment"; - private static final String KEY_CURRENT_TIME = "current_time"; - - // Affects when the month selection will change while scrolling up - protected static final int SCROLL_HYST_WEEKS = 2; - // How long the GoTo fling animation should last - protected static final int GOTO_SCROLL_DURATION = 500; - // How long to wait after receiving an onScrollStateChanged notification - // before acting on it - protected static final int SCROLL_CHANGE_DELAY = 40; - // The number of days to display in each week - public static final int DAYS_PER_WEEK = 7; - // The size of the month name displayed above the week list - protected static final int MINI_MONTH_NAME_TEXT_SIZE = 18; - public static int LIST_TOP_OFFSET = -1; // so that the top line will be under the separator - protected int WEEK_MIN_VISIBLE_HEIGHT = 12; - protected int BOTTOM_BUFFER = 20; - protected int mSaturdayColor = 0; - protected int mSundayColor = 0; - protected int mDayNameColor = 0; - - // You can override these numbers to get a different appearance - protected int mNumWeeks = 6; - protected boolean mShowWeekNumber = false; - protected int mDaysPerWeek = 7; - - // These affect the scroll speed and feel - protected float mFriction = 1.0f; - - protected Context mContext; - protected Handler mHandler; - - protected float mMinimumFlingVelocity; - - // highlighted time - protected Time mSelectedDay = new Time(); - protected SimpleWeeksAdapter mAdapter; - protected ListView mListView; - protected ViewGroup mDayNamesHeader; - protected String[] mDayLabels; - - // disposable variable used for time calculations - protected Time mTempTime = new Time(); - - private static float mScale = 0; - // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0). - protected int mFirstDayOfWeek; - // The first day of the focus month - protected Time mFirstDayOfMonth = new Time(); - // The first day that is visible in the view - protected Time mFirstVisibleDay = new Time(); - // The name of the month to display - protected TextView mMonthName; - // The last name announced by accessibility - protected CharSequence mPrevMonthName; - // which month should be displayed/highlighted [0-11] - protected int mCurrentMonthDisplayed; - // used for tracking during a scroll - protected long mPreviousScrollPosition; - // used for tracking which direction the view is scrolling - protected boolean mIsScrollingUp = false; - // used for tracking what state listview is in - protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; - // used for tracking what state listview is in - protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; - - // This causes an update of the view at midnight - protected Runnable mTodayUpdater = new Runnable() { - @Override - public void run() { - Time midnight = new Time(mFirstVisibleDay.timezone); - midnight.setToNow(); - long currentMillis = midnight.toMillis(true); - - midnight.hour = 0; - midnight.minute = 0; - midnight.second = 0; - midnight.monthDay++; - long millisToMidnight = midnight.normalize(true) - currentMillis; - mHandler.postDelayed(this, millisToMidnight); - - if (mAdapter != null) { - mAdapter.notifyDataSetChanged(); - } - } - }; - - // This allows us to update our position when a day is tapped - protected DataSetObserver mObserver = new DataSetObserver() { - @Override - public void onChanged() { - Time day = mAdapter.getSelectedDay(); - if (day.year != mSelectedDay.year || day.yearDay != mSelectedDay.yearDay) { - goTo(day.toMillis(true), true, true, false); - } - } - }; - - public SimpleDayPickerFragment(long initialTime) { - goTo(initialTime, false, true, true); - mHandler = new Handler(); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - mContext = activity; - String tz = Time.getCurrentTimezone(); - ViewConfiguration viewConfig = ViewConfiguration.get(activity); - mMinimumFlingVelocity = viewConfig.getScaledMinimumFlingVelocity(); - - // Ensure we're in the correct time zone - mSelectedDay.switchTimezone(tz); - mSelectedDay.normalize(true); - mFirstDayOfMonth.timezone = tz; - mFirstDayOfMonth.normalize(true); - mFirstVisibleDay.timezone = tz; - mFirstVisibleDay.normalize(true); - mTempTime.timezone = tz; - - Resources res = activity.getResources(); - mSaturdayColor = res.getColor(R.color.month_saturday); - mSundayColor = res.getColor(R.color.month_sunday); - mDayNameColor = res.getColor(R.color.month_day_names_color); - - // Adjust sizes for screen density - if (mScale == 0) { - mScale = activity.getResources().getDisplayMetrics().density; - if (mScale != 1) { - WEEK_MIN_VISIBLE_HEIGHT *= mScale; - BOTTOM_BUFFER *= mScale; - LIST_TOP_OFFSET *= mScale; - } - } - setUpAdapter(); - setListAdapter(mAdapter); - } - - /** - * Creates a new adapter if necessary and sets up its parameters. Override - * this method to provide a custom adapter. - */ - protected void setUpAdapter() { - HashMap<String, Integer> weekParams = new HashMap<String, Integer>(); - weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks); - weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, mShowWeekNumber ? 1 : 0); - weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek); - weekParams.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY, - Time.getJulianDay(mSelectedDay.toMillis(false), mSelectedDay.gmtoff)); - if (mAdapter == null) { - mAdapter = new SimpleWeeksAdapter(getActivity(), weekParams); - mAdapter.registerDataSetObserver(mObserver); - } else { - mAdapter.updateParams(weekParams); - } - // refresh the view with the new parameters - mAdapter.notifyDataSetChanged(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - setUpListView(); - setUpHeader(); - - mMonthName = (TextView) getView().findViewById(R.id.month_name); - SimpleWeekView child = (SimpleWeekView) mListView.getChildAt(0); - if (child == null) { - return; - } - int julianDay = child.getFirstJulianDay(); - mFirstVisibleDay.setJulianDay(julianDay); - // set the title to the month of the second week - mTempTime.setJulianDay(julianDay + DAYS_PER_WEEK); - setMonthDisplayed(mTempTime, true); - } - - /** - * Sets up the strings to be used by the header. Override this method to use - * different strings or modify the view params. - */ - protected void setUpHeader() { - mDayLabels = new String[7]; - for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { - mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(i, - DateUtils.LENGTH_SHORTEST).toUpperCase(); - } - } - - /** - * Sets all the required fields for the list view. Override this method to - * set a different list view behavior. - */ - protected void setUpListView() { - // Configure the listview - mListView = getListView(); - // Transparent background on scroll - mListView.setCacheColorHint(0); - // No dividers - mListView.setDivider(null); - // Items are clickable - mListView.setItemsCanFocus(true); - // The thumb gets in the way, so disable it - mListView.setFastScrollEnabled(false); - mListView.setVerticalScrollBarEnabled(false); - mListView.setOnScrollListener(this); - mListView.setFadingEdgeLength(0); - // Make the scrolling behavior nicer - mListView.setFriction(ViewConfiguration.getScrollFriction() * mFriction); - } - - @Override - public void onResume() { - super.onResume(); - setUpAdapter(); - doResumeUpdates(); - } - - @Override - public void onPause() { - super.onPause(); - mHandler.removeCallbacks(mTodayUpdater); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putLong(KEY_CURRENT_TIME, mSelectedDay.toMillis(true)); - } - - /** - * Updates the user preference fields. Override this to use a different - * preference space. - */ - protected void doResumeUpdates() { - // Get default week start based on locale, subtracting one for use with android Time. - Calendar cal = Calendar.getInstance(Locale.getDefault()); - mFirstDayOfWeek = cal.getFirstDayOfWeek() - 1; - - mShowWeekNumber = false; - - updateHeader(); - goTo(mSelectedDay.toMillis(true), false, false, false); - mAdapter.setSelectedDay(mSelectedDay); - mTodayUpdater.run(); - } - - /** - * Fixes the day names header to provide correct spacing and updates the - * label text. Override this to set up a custom header. - */ - protected void updateHeader() { - TextView label = (TextView) mDayNamesHeader.findViewById(R.id.wk_label); - if (mShowWeekNumber) { - label.setVisibility(View.VISIBLE); - } else { - label.setVisibility(View.GONE); - } - int offset = mFirstDayOfWeek - 1; - for (int i = 1; i < 8; i++) { - label = (TextView) mDayNamesHeader.getChildAt(i); - if (i < mDaysPerWeek + 1) { - int position = (offset + i) % 7; - label.setText(mDayLabels[position]); - label.setVisibility(View.VISIBLE); - if (position == Time.SATURDAY) { - label.setTextColor(mSaturdayColor); - } else if (position == Time.SUNDAY) { - label.setTextColor(mSundayColor); - } else { - label.setTextColor(mDayNameColor); - } - } else { - label.setVisibility(View.GONE); - } - } - mDayNamesHeader.invalidate(); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.month_by_week, - container, false); - mDayNamesHeader = (ViewGroup) v.findViewById(R.id.day_names); - return v; - } - - /** - * Returns the UTC millis since epoch representation of the currently - * selected time. - * - * @return - */ - public long getSelectedTime() { - return mSelectedDay.toMillis(true); - } - - /** - * This moves to the specified time in the view. If the time is not already - * in range it will move the list so that the first of the month containing - * the time is at the top of the view. If the new time is already in view - * the list will not be scrolled unless forceScroll is true. This time may - * optionally be highlighted as selected as well. - * - * @param time The time to move to - * @param animate Whether to scroll to the given time or just redraw at the - * new location - * @param setSelected Whether to set the given time as selected - * @param forceScroll Whether to recenter even if the time is already - * visible - * @return Whether or not the view animated to the new location - */ - public boolean goTo(long time, boolean animate, boolean setSelected, boolean forceScroll) { - if (time == -1) { - Log.e(TAG, "time is invalid"); - return false; - } - - // Set the selected day - if (setSelected) { - mSelectedDay.set(time); - mSelectedDay.normalize(true); - } - - // If this view isn't returned yet we won't be able to load the lists - // current position, so return after setting the selected day. - if (!isResumed()) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "We're not visible yet"); - } - return false; - } - - mTempTime.set(time); - long millis = mTempTime.normalize(true); - // Get the week we're going to - // TODO push Util function into Calendar public api. - int position = Utils.getWeeksSinceEpochFromJulianDay( - Time.getJulianDay(millis, mTempTime.gmtoff), mFirstDayOfWeek); - - View child; - int i = 0; - int top = 0; - // Find a child that's completely in the view - do { - child = mListView.getChildAt(i++); - if (child == null) { - break; - } - top = child.getTop(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "child at " + (i-1) + " has top " + top); - } - } while (top < 0); - - // Compute the first and last position visible - int firstPosition; - if (child != null) { - firstPosition = mListView.getPositionForView(child); - } else { - firstPosition = 0; - } - int lastPosition = firstPosition + mNumWeeks - 1; - if (top > BOTTOM_BUFFER) { - lastPosition--; - } - - if (setSelected) { - mAdapter.setSelectedDay(mSelectedDay); - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "GoTo position " + position); - } - // Check if the selected day is now outside of our visible range - // and if so scroll to the month that contains it - if (position < firstPosition || position > lastPosition || forceScroll) { - mFirstDayOfMonth.set(mTempTime); - mFirstDayOfMonth.monthDay = 1; - millis = mFirstDayOfMonth.normalize(true); - setMonthDisplayed(mFirstDayOfMonth, true); - position = Utils.getWeeksSinceEpochFromJulianDay( - Time.getJulianDay(millis, mFirstDayOfMonth.gmtoff), mFirstDayOfWeek); - - mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; - if (animate) { - mListView.smoothScrollToPositionFromTop( - position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION); - return true; - } else { - mListView.setSelectionFromTop(position, LIST_TOP_OFFSET); - // Perform any after scroll operations that are needed - onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE); - } - } else if (setSelected) { - // Otherwise just set the selection - setMonthDisplayed(mSelectedDay, true); - } - return false; - } - - /** - * Updates the title and selected month if the view has moved to a new - * month. - */ - @Override - public void onScroll( - AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - SimpleWeekView child = (SimpleWeekView)view.getChildAt(0); - if (child == null) { - return; - } - - // Figure out where we are - long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); - mFirstVisibleDay.setJulianDay(child.getFirstJulianDay()); - - // If we have moved since our last call update the direction - if (currScroll < mPreviousScrollPosition) { - mIsScrollingUp = true; - } else if (currScroll > mPreviousScrollPosition) { - mIsScrollingUp = false; - } else { - return; - } - - mPreviousScrollPosition = currScroll; - mPreviousScrollState = mCurrentScrollState; - - updateMonthHighlight(mListView); - } - - /** - * Figures out if the month being shown has changed and updates the - * highlight if needed - * - * @param view The ListView containing the weeks - */ - private void updateMonthHighlight(AbsListView view) { - SimpleWeekView child = (SimpleWeekView) view.getChildAt(0); - if (child == null) { - return; - } - - // Figure out where we are - int offset = child.getBottom() < WEEK_MIN_VISIBLE_HEIGHT ? 1 : 0; - // Use some hysteresis for checking which month to highlight. This - // causes the month to transition when two full weeks of a month are - // visible. - child = (SimpleWeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset); - - if (child == null) { - return; - } - - // Find out which month we're moving into - int month; - if (mIsScrollingUp) { - month = child.getFirstMonth(); - } else { - month = child.getLastMonth(); - } - - // And how it relates to our current highlighted month - int monthDiff; - if (mCurrentMonthDisplayed == 11 && month == 0) { - monthDiff = 1; - } else if (mCurrentMonthDisplayed == 0 && month == 11) { - monthDiff = -1; - } else { - monthDiff = month - mCurrentMonthDisplayed; - } - - // Only switch months if we're scrolling away from the currently - // selected month - if (monthDiff != 0) { - int julianDay = child.getFirstJulianDay(); - if (mIsScrollingUp) { - // Takes the start of the week - } else { - // Takes the start of the following week - julianDay += DAYS_PER_WEEK; - } - mTempTime.setJulianDay(julianDay); - setMonthDisplayed(mTempTime, false); - } - } - - /** - * Sets the month displayed at the top of this view based on time. Override - * to add custom events when the title is changed. - * - * @param time A day in the new focus month. - * @param updateHighlight TODO(epastern): - */ - protected void setMonthDisplayed(Time time, boolean updateHighlight) { - CharSequence oldMonth = mMonthName.getText(); - mMonthName.setText(Utils.formatMonthYear(mContext, time)); - mMonthName.invalidate(); - if (!TextUtils.equals(oldMonth, mMonthName.getText())) { - mMonthName.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - } - mCurrentMonthDisplayed = time.month; - if (updateHighlight) { - mAdapter.updateFocusMonth(mCurrentMonthDisplayed); - } - } - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - // use a post to prevent re-entering onScrollStateChanged before it - // exits - mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); - } - - protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); - - protected class ScrollStateRunnable implements Runnable { - private int mNewState; - - /** - * Sets up the runnable with a short delay in case the scroll state - * immediately changes again. - * - * @param view The list view that changed state - * @param scrollState The new state it changed to - */ - public void doScrollStateChange(AbsListView view, int scrollState) { - mHandler.removeCallbacks(this); - mNewState = scrollState; - mHandler.postDelayed(this, SCROLL_CHANGE_DELAY); - } - - public void run() { - mCurrentScrollState = mNewState; - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, - "new scroll state: " + mNewState + " old state: " + mPreviousScrollState); - } - // Fix the position after a scroll or a fling ends - if (mNewState == OnScrollListener.SCROLL_STATE_IDLE - && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { - mPreviousScrollState = mNewState; - mAdapter.updateFocusMonth(mCurrentMonthDisplayed); - } else { - mPreviousScrollState = mNewState; - } - } - } -} diff --git a/src/com/android/calendar/month/SimpleDayPickerFragment.kt b/src/com/android/calendar/month/SimpleDayPickerFragment.kt new file mode 100644 index 00000000..01fcbac6 --- /dev/null +++ b/src/com/android/calendar/month/SimpleDayPickerFragment.kt @@ -0,0 +1,616 @@ +/* + * Copyright (C) 2021 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.calendar.month + +import com.android.calendar.R +import com.android.calendar.Utils +import android.app.Activity +import android.app.ListFragment +import android.content.Context +import android.content.res.Resources +import android.database.DataSetObserver +import android.os.Bundle +import android.os.Handler +import android.text.TextUtils +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent +import android.widget.AbsListView +import android.widget.AbsListView.OnScrollListener +import android.widget.ListView +import android.widget.TextView +import java.util.Calendar +import java.util.HashMap +import java.util.Locale + +/** + * + * + * This displays a titled list of weeks with selectable days. It can be + * configured to display the week number, start the week on a given day, show a + * reduced number of days, or display an arbitrary number of weeks at a time. By + * overriding methods and changing variables this fragment can be customized to + * easily display a month selection component in a given style. + * + */ +open class SimpleDayPickerFragment(initialTime: Long) : ListFragment(), OnScrollListener { + protected var WEEK_MIN_VISIBLE_HEIGHT = 12 + protected var BOTTOM_BUFFER = 20 + protected var mSaturdayColor = 0 + protected var mSundayColor = 0 + protected var mDayNameColor = 0 + + // You can override these numbers to get a different appearance + @JvmField protected var mNumWeeks = 6 + @JvmField protected var mShowWeekNumber = false + @JvmField protected var mDaysPerWeek = 7 + + // These affect the scroll speed and feel + protected var mFriction = 1.0f + @JvmField protected var mContext: Context? = null + @JvmField protected var mHandler: Handler = Handler() + protected var mMinimumFlingVelocity = 0f + + // highlighted time + @JvmField protected var mSelectedDay: Time = Time() + @JvmField protected var mAdapter: SimpleWeeksAdapter? = null + @JvmField protected var mListView: ListView? = null + @JvmField protected var mDayNamesHeader: ViewGroup? = null + @JvmField protected var mDayLabels: Array<String?> = arrayOfNulls(7) + + // disposable variable used for time calculations + @JvmField protected var mTempTime: Time = Time() + + // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0). + @JvmField protected var mFirstDayOfWeek = 0 + + // The first day of the focus month + @JvmField protected var mFirstDayOfMonth: Time = Time() + + // The first day that is visible in the view + @JvmField protected var mFirstVisibleDay: Time = Time() + + // The name of the month to display + protected var mMonthName: TextView? = null + + // The last name announced by accessibility + protected var mPrevMonthName: CharSequence? = null + + // which month should be displayed/highlighted [0-11] + protected var mCurrentMonthDisplayed = 0 + + // used for tracking during a scroll + protected var mPreviousScrollPosition: Long = 0 + + // used for tracking which direction the view is scrolling + protected var mIsScrollingUp = false + + // used for tracking what state listview is in + protected var mPreviousScrollState: Int = OnScrollListener.SCROLL_STATE_IDLE + + // used for tracking what state listview is in + protected var mCurrentScrollState: Int = OnScrollListener.SCROLL_STATE_IDLE + + // This causes an update of the view at midnight + @JvmField protected var mTodayUpdater: Runnable = object : Runnable { + @Override + override fun run() { + val midnight = Time(mFirstVisibleDay.timezone) + midnight.setToNow() + val currentMillis: Long = midnight.toMillis(true) + midnight.hour = 0 + midnight.minute = 0 + midnight.second = 0 + midnight.monthDay++ + val millisToMidnight: Long = midnight.normalize(true) - currentMillis + mHandler?.postDelayed(this, millisToMidnight) + if (mAdapter != null) { + mAdapter?.notifyDataSetChanged() + } + } + } + + // This allows us to update our position when a day is tapped + @JvmField protected var mObserver: DataSetObserver = object : DataSetObserver() { + @Override + override fun onChanged() { + val day: Time? = mAdapter!!.getSelectedDay() + if (day!!.year !== mSelectedDay!!.year || day!!.yearDay !== mSelectedDay.yearDay) { + goTo(day!!.toMillis(true), true, true, false) + } + } + } + + @Override + override fun onAttach(activity: Activity) { + super.onAttach(activity) + mContext = activity + val tz: String = Time.getCurrentTimezone() + val viewConfig: ViewConfiguration = ViewConfiguration.get(activity) + mMinimumFlingVelocity = (viewConfig.getScaledMinimumFlingVelocity()).toFloat() + + // Ensure we're in the correct time zone + mSelectedDay.switchTimezone(tz) + mSelectedDay.normalize(true) + mFirstDayOfMonth.timezone = tz + mFirstDayOfMonth.normalize(true) + mFirstVisibleDay.timezone = tz + mFirstVisibleDay.normalize(true) + mTempTime.timezone = tz + val res: Resources = activity.getResources() + mSaturdayColor = res.getColor(R.color.month_saturday) + mSundayColor = res.getColor(R.color.month_sunday) + mDayNameColor = res.getColor(R.color.month_day_names_color) + + // Adjust sizes for screen density + if (mScale == 0f) { + mScale = activity.getResources().getDisplayMetrics().density + if (mScale != 1f) { + WEEK_MIN_VISIBLE_HEIGHT *= mScale.toInt() + BOTTOM_BUFFER *= mScale.toInt() + LIST_TOP_OFFSET *= mScale.toInt() + } + } + setUpAdapter() + setListAdapter(mAdapter) + } + + /** + * Creates a new adapter if necessary and sets up its parameters. Override + * this method to provide a custom adapter. + */ + protected open fun setUpAdapter() { + val weekParams = HashMap<String?, Int?>() + weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_NUM_WEEKS, mNumWeeks) + weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_SHOW_WEEK, if (mShowWeekNumber) 1 else 0) + weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_WEEK_START, mFirstDayOfWeek) + weekParams?.put(SimpleWeeksAdapter.WEEK_PARAMS_JULIAN_DAY, + Time.getJulianDay(mSelectedDay.toMillis(false), mSelectedDay.gmtoff)) + if (mAdapter == null) { + mAdapter = SimpleWeeksAdapter(getActivity(), weekParams) + mAdapter?.registerDataSetObserver(mObserver) + } else { + mAdapter?.updateParams(weekParams) + } + // refresh the view with the new parameters + mAdapter?.notifyDataSetChanged() + } + + @Override + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + @Override + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + setUpListView() + setUpHeader() + mMonthName = getView()?.findViewById(R.id.month_name) as? TextView + val child = mListView?.getChildAt(0) as? SimpleWeekView + if (child == null) { + return + } + val julianDay: Int = child.getFirstJulianDay() + mFirstVisibleDay.setJulianDay(julianDay) + // set the title to the month of the second week + mTempTime.setJulianDay(julianDay + DAYS_PER_WEEK) + setMonthDisplayed(mTempTime, true) + } + + /** + * Sets up the strings to be used by the header. Override this method to use + * different strings or modify the view params. + */ + protected open fun setUpHeader() { + mDayLabels = arrayOfNulls(7) + for (i in Calendar.SUNDAY..Calendar.SATURDAY) { + mDayLabels[i - Calendar.SUNDAY] = DateUtils.getDayOfWeekString(i, + DateUtils.LENGTH_SHORTEST).toUpperCase() + } + } + + /** + * Sets all the required fields for the list view. Override this method to + * set a different list view behavior. + */ + protected fun setUpListView() { + // Configure the listview + mListView = getListView() + // Transparent background on scroll + mListView?.setCacheColorHint(0) + // No dividers + mListView?.setDivider(null) + // Items are clickable + mListView?.setItemsCanFocus(true) + // The thumb gets in the way, so disable it + mListView?.setFastScrollEnabled(false) + mListView?.setVerticalScrollBarEnabled(false) + mListView?.setOnScrollListener(this) + mListView?.setFadingEdgeLength(0) + // Make the scrolling behavior nicer + mListView?.setFriction(ViewConfiguration.getScrollFriction() * mFriction) + } + + @Override + override fun onResume() { + super.onResume() + setUpAdapter() + doResumeUpdates() + } + + @Override + override fun onPause() { + super.onPause() + mHandler.removeCallbacks(mTodayUpdater) + } + + @Override + override fun onSaveInstanceState(outState: Bundle) { + outState.putLong(KEY_CURRENT_TIME, mSelectedDay.toMillis(true)) + } + + /** + * Updates the user preference fields. Override this to use a different + * preference space. + */ + protected open fun doResumeUpdates() { + // Get default week start based on locale, subtracting one for use with android Time. + val cal: Calendar = Calendar.getInstance(Locale.getDefault()) + mFirstDayOfWeek = cal.getFirstDayOfWeek() - 1 + mShowWeekNumber = false + updateHeader() + goTo(mSelectedDay.toMillis(true), false, false, false) + mAdapter?.setSelectedDay(mSelectedDay) + mTodayUpdater.run() + } + + /** + * Fixes the day names header to provide correct spacing and updates the + * label text. Override this to set up a custom header. + */ + protected fun updateHeader() { + var label: TextView = mDayNamesHeader!!.findViewById(R.id.wk_label) as TextView + if (mShowWeekNumber) { + label.setVisibility(View.VISIBLE) + } else { + label.setVisibility(View.GONE) + } + val offset = mFirstDayOfWeek - 1 + for (i in 1..7) { + label = mDayNamesHeader!!.getChildAt(i) as TextView + if (i < mDaysPerWeek + 1) { + val position = (offset + i) % 7 + label.setText(mDayLabels[position]) + label.setVisibility(View.VISIBLE) + if (position == Time.SATURDAY) { + label.setTextColor(mSaturdayColor) + } else if (position == Time.SUNDAY) { + label.setTextColor(mSundayColor) + } else { + label.setTextColor(mDayNameColor) + } + } else { + label.setVisibility(View.GONE) + } + } + mDayNamesHeader?.invalidate() + } + + @Override + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val v: View = inflater.inflate(R.layout.month_by_week, + container, false) + mDayNamesHeader = v.findViewById(R.id.day_names) as ViewGroup + return v + } + + /** + * Returns the UTC millis since epoch representation of the currently + * selected time. + * + * @return + */ + val selectedTime: Long + get() = mSelectedDay.toMillis(true) + + /** + * This moves to the specified time in the view. If the time is not already + * in range it will move the list so that the first of the month containing + * the time is at the top of the view. If the new time is already in view + * the list will not be scrolled unless forceScroll is true. This time may + * optionally be highlighted as selected as well. + * + * @param time The time to move to + * @param animate Whether to scroll to the given time or just redraw at the + * new location + * @param setSelected Whether to set the given time as selected + * @param forceScroll Whether to recenter even if the time is already + * visible + * @return Whether or not the view animated to the new location + */ + fun goTo(time: Long, animate: Boolean, setSelected: Boolean, forceScroll: Boolean): Boolean { + if (time == -1L) { + Log.e(TAG, "time is invalid") + return false + } + + // Set the selected day + if (setSelected) { + mSelectedDay.set(time) + mSelectedDay.normalize(true) + } + + // If this view isn't returned yet we won't be able to load the lists + // current position, so return after setting the selected day. + if (!isResumed()) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "We're not visible yet") + } + return false + } + mTempTime.set(time) + var millis: Long = mTempTime.normalize(true) + // Get the week we're going to + // TODO push Util function into Calendar public api. + var position: Int = Utils.getWeeksSinceEpochFromJulianDay( + Time.getJulianDay(millis, mTempTime.gmtoff), mFirstDayOfWeek) + var child: View? + var i = 0 + var top = 0 + // Find a child that's completely in the view + do { + child = mListView?.getChildAt(i++) + if (child == null) { + break + } + top = child.getTop() + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "child at " + (i - 1) + " has top " + top) + } + } while (top < 0) + + // Compute the first and last position visible + val firstPosition: Int + firstPosition = if (child != null) { + mListView!!.getPositionForView(child) + } else { + 0 + } + var lastPosition = firstPosition + mNumWeeks - 1 + if (top > BOTTOM_BUFFER) { + lastPosition-- + } + if (setSelected) { + mAdapter?.setSelectedDay(mSelectedDay) + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "GoTo position $position") + } + // Check if the selected day is now outside of our visible range + // and if so scroll to the month that contains it + if (position < firstPosition || position > lastPosition || forceScroll) { + mFirstDayOfMonth.set(mTempTime) + mFirstDayOfMonth.monthDay = 1 + millis = mFirstDayOfMonth.normalize(true) + setMonthDisplayed(mFirstDayOfMonth, true) + position = Utils.getWeeksSinceEpochFromJulianDay( + Time.getJulianDay(millis, mFirstDayOfMonth.gmtoff), mFirstDayOfWeek) + mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING + if (animate) { + mListView?.smoothScrollToPositionFromTop( + position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION) + return true + } else { + mListView?.setSelectionFromTop(position, LIST_TOP_OFFSET) + // Perform any after scroll operations that are needed + onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE) + } + } else if (setSelected) { + // Otherwise just set the selection + setMonthDisplayed(mSelectedDay, true) + } + return false + } + + /** + * Updates the title and selected month if the view has moved to a new + * month. + */ + @Override + override fun onScroll( + view: AbsListView, + firstVisibleItem: Int, + visibleItemCount: Int, + totalItemCount: Int + ) { + val child = view.getChildAt(0) as? SimpleWeekView + if (child == null) { + return + } + + // Figure out where we are + val currScroll: Long = (view.getFirstVisiblePosition() * child.getHeight() - + child.getBottom()).toLong() + mFirstVisibleDay.setJulianDay(child.getFirstJulianDay()) + + // If we have moved since our last call update the direction + mIsScrollingUp = if (currScroll < mPreviousScrollPosition) { + true + } else if (currScroll > mPreviousScrollPosition) { + false + } else { + return + } + mPreviousScrollPosition = currScroll + mPreviousScrollState = mCurrentScrollState + updateMonthHighlight(mListView as? AbsListView) + } + + /** + * Figures out if the month being shown has changed and updates the + * highlight if needed + * + * @param view The ListView containing the weeks + */ + private fun updateMonthHighlight(view: AbsListView?) { + var child = view?.getChildAt(0) as? SimpleWeekView + if (child == null) { + return + } + + // Figure out where we are + val offset = if (child?.getBottom() < WEEK_MIN_VISIBLE_HEIGHT) 1 else 0 + // Use some hysteresis for checking which month to highlight. This + // causes the month to transition when two full weeks of a month are + // visible. + child = view?.getChildAt(SCROLL_HYST_WEEKS + offset) as? SimpleWeekView + if (child == null) { + return + } + + // Find out which month we're moving into + val month: Int + month = if (mIsScrollingUp) { + child?.getFirstMonth() + } else { + child?.getLastMonth() + } + + // And how it relates to our current highlighted month + val monthDiff: Int + monthDiff = if (mCurrentMonthDisplayed == 11 && month == 0) { + 1 + } else if (mCurrentMonthDisplayed == 0 && month == 11) { + -1 + } else { + month - mCurrentMonthDisplayed + } + + // Only switch months if we're scrolling away from the currently + // selected month + if (monthDiff != 0) { + var julianDay: Int = child.getFirstJulianDay() + if (mIsScrollingUp) { + // Takes the start of the week + } else { + // Takes the start of the following week + julianDay += DAYS_PER_WEEK + } + mTempTime.setJulianDay(julianDay) + setMonthDisplayed(mTempTime, false) + } + } + + /** + * Sets the month displayed at the top of this view based on time. Override + * to add custom events when the title is changed. + * + * @param time A day in the new focus month. + * @param updateHighlight TODO(epastern): + */ + protected open fun setMonthDisplayed(time: Time, updateHighlight: Boolean) { + val oldMonth: CharSequence = mMonthName!!.getText() + mMonthName?.setText(Utils.formatMonthYear(mContext, time)) + mMonthName?.invalidate() + if (!TextUtils.equals(oldMonth, mMonthName?.getText())) { + mMonthName?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + mCurrentMonthDisplayed = time.month + if (updateHighlight) { + mAdapter?.updateFocusMonth(mCurrentMonthDisplayed) + } + } + + @Override + override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) { + // use a post to prevent re-entering onScrollStateChanged before it + // exits + mScrollStateChangedRunnable.doScrollStateChange(view, scrollState) + } + + @JvmField protected var mScrollStateChangedRunnable: ScrollStateRunnable = ScrollStateRunnable() + + protected inner class ScrollStateRunnable : Runnable { + private var mNewState = 0 + + /** + * Sets up the runnable with a short delay in case the scroll state + * immediately changes again. + * + * @param view The list view that changed state + * @param scrollState The new state it changed to + */ + fun doScrollStateChange(view: AbsListView?, scrollState: Int) { + mHandler.removeCallbacks(this) + mNewState = scrollState + mHandler.postDelayed(this, SCROLL_CHANGE_DELAY.toLong()) + } + + override fun run() { + mCurrentScrollState = mNewState + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, + "new scroll state: $mNewState old state: $mPreviousScrollState") + } + // Fix the position after a scroll or a fling ends + if (mNewState == OnScrollListener.SCROLL_STATE_IDLE && + mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { + mPreviousScrollState = mNewState + mAdapter?.updateFocusMonth(mCurrentMonthDisplayed) + } else { + mPreviousScrollState = mNewState + } + } + } + + companion object { + private const val TAG = "MonthFragment" + private const val KEY_CURRENT_TIME = "current_time" + + // Affects when the month selection will change while scrolling up + protected const val SCROLL_HYST_WEEKS = 2 + + // How long the GoTo fling animation should last + @JvmStatic protected val GOTO_SCROLL_DURATION = 500 + + // How long to wait after receiving an onScrollStateChanged notification + // before acting on it + protected const val SCROLL_CHANGE_DELAY = 40 + + // The number of days to display in each week + const val DAYS_PER_WEEK = 7 + + // The size of the month name displayed above the week list + protected const val MINI_MONTH_NAME_TEXT_SIZE = 18 + var LIST_TOP_OFFSET = -1 // so that the top line will be under the separator + private var mScale = 0f + } + + init { + goTo(initialTime, false, true, true) + mHandler = Handler() + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/month/SimpleWeekView.java b/src/com/android/calendar/month/SimpleWeekView.java deleted file mode 100644 index 4d0c09f4..00000000 --- a/src/com/android/calendar/month/SimpleWeekView.java +++ /dev/null @@ -1,551 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar.month; - -import com.android.calendar.R; -import com.android.calendar.Utils; - -import android.app.Service; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Paint.Align; -import android.graphics.Paint.Style; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.view.MotionEvent; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; - -import java.security.InvalidParameterException; -import java.util.HashMap; - -/** - * <p> - * This is a dynamic view for drawing a single week. It can be configured to - * display the week number, start the week on a given day, or show a reduced - * number of days. It is intended for use as a single view within a ListView. - * See {@link SimpleWeeksAdapter} for usage. - * </p> - */ -public class SimpleWeekView extends View { - private static final String TAG = "MonthView"; - - /** - * These params can be passed into the view to control how it appears. - * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default - * values are unlikely to fit most layouts correctly. - */ - /** - * This sets the height of this week in pixels - */ - public static final String VIEW_PARAMS_HEIGHT = "height"; - /** - * This specifies the position (or weeks since the epoch) of this week, - * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay} - */ - public static final String VIEW_PARAMS_WEEK = "week"; - /** - * This sets one of the days in this view as selected {@link Time#SUNDAY} - * through {@link Time#SATURDAY}. - */ - public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; - /** - * Which day the week should start on. {@link Time#SUNDAY} through - * {@link Time#SATURDAY}. - */ - public static final String VIEW_PARAMS_WEEK_START = "week_start"; - /** - * How many days to display at a time. Days will be displayed starting with - * {@link #mWeekStart}. - */ - public static final String VIEW_PARAMS_NUM_DAYS = "num_days"; - /** - * Which month is currently in focus, as defined by {@link Time#month} - * [0-11]. - */ - public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month"; - /** - * If this month should display week numbers. false if 0, true otherwise. - */ - public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num"; - - protected static int DEFAULT_HEIGHT = 32; - protected static int MIN_HEIGHT = 10; - protected static final int DEFAULT_SELECTED_DAY = -1; - protected static final int DEFAULT_WEEK_START = Time.SUNDAY; - protected static final int DEFAULT_NUM_DAYS = 7; - protected static final int DEFAULT_SHOW_WK_NUM = 0; - protected static final int DEFAULT_FOCUS_MONTH = -1; - - protected static int DAY_SEPARATOR_WIDTH = 1; - - protected static int MINI_DAY_NUMBER_TEXT_SIZE = 14; - protected static int MINI_WK_NUMBER_TEXT_SIZE = 12; - protected static int MINI_TODAY_NUMBER_TEXT_SIZE = 18; - protected static int MINI_TODAY_OUTLINE_WIDTH = 2; - protected static int WEEK_NUM_MARGIN_BOTTOM = 4; - - // used for scaling to the device density - protected static float mScale = 0; - - // affects the padding on the sides of this view - protected int mPadding = 0; - - protected Rect r = new Rect(); - protected Paint p = new Paint(); - protected Paint mMonthNumPaint; - protected Drawable mSelectedDayLine; - - // Cache the number strings so we don't have to recompute them each time - protected String[] mDayNumbers; - // Quick lookup for checking which days are in the focus month - protected boolean[] mFocusDay; - // Quick lookup for checking which days are in an odd month (to set a different background) - protected boolean[] mOddMonth; - // The Julian day of the first day displayed by this item - protected int mFirstJulianDay = -1; - // The month of the first day in this week - protected int mFirstMonth = -1; - // The month of the last day in this week - protected int mLastMonth = -1; - // The position of this week, equivalent to weeks since the week of Jan 1st, - // 1970 - protected int mWeek = -1; - // Quick reference to the width of this view, matches parent - protected int mWidth; - // The height this view should draw at in pixels, set by height param - protected int mHeight = DEFAULT_HEIGHT; - // Whether the week number should be shown - protected boolean mShowWeekNum = false; - // If this view contains the selected day - protected boolean mHasSelectedDay = false; - // If this view contains the today - protected boolean mHasToday = false; - // Which day is selected [0-6] or -1 if no day is selected - protected int mSelectedDay = DEFAULT_SELECTED_DAY; - // Which day is today [0-6] or -1 if no day is today - protected int mToday = DEFAULT_SELECTED_DAY; - // Which day of the week to start on [0-6] - protected int mWeekStart = DEFAULT_WEEK_START; - // How many days to display - protected int mNumDays = DEFAULT_NUM_DAYS; - // The number of days + a spot for week number if it is displayed - protected int mNumCells = mNumDays; - // The left edge of the selected day - protected int mSelectedLeft = -1; - // The right edge of the selected day - protected int mSelectedRight = -1; - // The timezone to display times/dates in (used for determining when Today - // is) - protected String mTimeZone = Time.getCurrentTimezone(); - - protected int mBGColor; - protected int mSelectedWeekBGColor; - protected int mFocusMonthColor; - protected int mOtherMonthColor; - protected int mDaySeparatorColor; - protected int mTodayOutlineColor; - protected int mWeekNumColor; - - public SimpleWeekView(Context context) { - super(context); - - Resources res = context.getResources(); - - mBGColor = res.getColor(R.color.month_bgcolor); - mSelectedWeekBGColor = res.getColor(R.color.month_selected_week_bgcolor); - mFocusMonthColor = res.getColor(R.color.month_mini_day_number); - mOtherMonthColor = res.getColor(R.color.month_other_month_day_number); - mDaySeparatorColor = res.getColor(R.color.month_grid_lines); - mTodayOutlineColor = res.getColor(R.color.mini_month_today_outline_color); - mWeekNumColor = res.getColor(R.color.month_week_num_color); - mSelectedDayLine = res.getDrawable(R.drawable.dayline_minical_holo_light); - - if (mScale == 0) { - mScale = context.getResources().getDisplayMetrics().density; - if (mScale != 1) { - DEFAULT_HEIGHT *= mScale; - MIN_HEIGHT *= mScale; - MINI_DAY_NUMBER_TEXT_SIZE *= mScale; - MINI_TODAY_NUMBER_TEXT_SIZE *= mScale; - MINI_TODAY_OUTLINE_WIDTH *= mScale; - WEEK_NUM_MARGIN_BOTTOM *= mScale; - DAY_SEPARATOR_WIDTH *= mScale; - MINI_WK_NUMBER_TEXT_SIZE *= mScale; - } - } - - // Sets up any standard paints that will be used - initView(); - } - - /** - * Sets all the parameters for displaying this week. The only required - * parameter is the week number. Other parameters have a default value and - * will only update if a new value is included, except for focus month, - * which will always default to no focus month if no value is passed in. See - * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters. - * - * @param params A map of the new parameters, see - * {@link #VIEW_PARAMS_HEIGHT} - * @param tz The time zone this view should reference times in - */ - public void setWeekParams(HashMap<String, Integer> params, String tz) { - if (!params.containsKey(VIEW_PARAMS_WEEK)) { - throw new InvalidParameterException("You must specify the week number for this view"); - } - setTag(params); - mTimeZone = tz; - // We keep the current value for any params not present - if (params.containsKey(VIEW_PARAMS_HEIGHT)) { - mHeight = params.get(VIEW_PARAMS_HEIGHT); - if (mHeight < MIN_HEIGHT) { - mHeight = MIN_HEIGHT; - } - } - if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { - mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY); - } - mHasSelectedDay = mSelectedDay != -1; - if (params.containsKey(VIEW_PARAMS_NUM_DAYS)) { - mNumDays = params.get(VIEW_PARAMS_NUM_DAYS); - } - if (params.containsKey(VIEW_PARAMS_SHOW_WK_NUM)) { - if (params.get(VIEW_PARAMS_SHOW_WK_NUM) != 0) { - mShowWeekNum = true; - } else { - mShowWeekNum = false; - } - } - mNumCells = mShowWeekNum ? mNumDays + 1 : mNumDays; - - // Allocate space for caching the day numbers and focus values - mDayNumbers = new String[mNumCells]; - mFocusDay = new boolean[mNumCells]; - mOddMonth = new boolean[mNumCells]; - mWeek = params.get(VIEW_PARAMS_WEEK); - int julianMonday = Utils.getJulianMondayFromWeeksSinceEpoch(mWeek); - Time time = new Time(tz); - time.setJulianDay(julianMonday); - - // If we're showing the week number calculate it based on Monday - int i = 0; - if (mShowWeekNum) { - mDayNumbers[0] = Integer.toString(time.getWeekNumber()); - i++; - } - - if (params.containsKey(VIEW_PARAMS_WEEK_START)) { - mWeekStart = params.get(VIEW_PARAMS_WEEK_START); - } - - // Now adjust our starting day based on the start day of the week - // If the week is set to start on a Saturday the first week will be - // Dec 27th 1969 -Jan 2nd, 1970 - if (time.weekDay != mWeekStart) { - int diff = time.weekDay - mWeekStart; - if (diff < 0) { - diff += 7; - } - time.monthDay -= diff; - time.normalize(true); - } - - mFirstJulianDay = Time.getJulianDay(time.toMillis(true), time.gmtoff); - mFirstMonth = time.month; - - // Figure out what day today is - Time today = new Time(tz); - today.setToNow(); - mHasToday = false; - mToday = -1; - - int focusMonth = params.containsKey(VIEW_PARAMS_FOCUS_MONTH) ? params.get( - VIEW_PARAMS_FOCUS_MONTH) - : DEFAULT_FOCUS_MONTH; - - for (; i < mNumCells; i++) { - if (time.monthDay == 1) { - mFirstMonth = time.month; - } - mOddMonth [i] = (time.month %2) == 1; - if (time.month == focusMonth) { - mFocusDay[i] = true; - } else { - mFocusDay[i] = false; - } - if (time.year == today.year && time.yearDay == today.yearDay) { - mHasToday = true; - mToday = i; - } - mDayNumbers[i] = Integer.toString(time.monthDay++); - time.normalize(true); - } - // We do one extra add at the end of the loop, if that pushed us to a - // new month undo it - if (time.monthDay == 1) { - time.monthDay--; - time.normalize(true); - } - mLastMonth = time.month; - - updateSelectionPositions(); - } - - /** - * Sets up the text and style properties for painting. Override this if you - * want to use a different paint. - */ - protected void initView() { - p.setFakeBoldText(false); - p.setAntiAlias(true); - p.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); - p.setStyle(Style.FILL); - - mMonthNumPaint = new Paint(); - mMonthNumPaint.setFakeBoldText(true); - mMonthNumPaint.setAntiAlias(true); - mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); - mMonthNumPaint.setColor(mFocusMonthColor); - mMonthNumPaint.setStyle(Style.FILL); - mMonthNumPaint.setTextAlign(Align.CENTER); - } - - /** - * Returns the month of the first day in this week - * - * @return The month the first day of this view is in - */ - public int getFirstMonth() { - return mFirstMonth; - } - - /** - * Returns the month of the last day in this week - * - * @return The month the last day of this view is in - */ - public int getLastMonth() { - return mLastMonth; - } - - /** - * Returns the julian day of the first day in this view. - * - * @return The julian day of the first day in the view. - */ - public int getFirstJulianDay() { - return mFirstJulianDay; - } - - /** - * Calculates the day that the given x position is in, accounting for week - * number. Returns a Time referencing that day or null if - * - * @param x The x position of the touch event - * @return A time object for the tapped day or null if the position wasn't - * in a day - */ - public Time getDayFromLocation(float x) { - int dayStart = mShowWeekNum ? (mWidth - mPadding * 2) / mNumCells + mPadding : mPadding; - if (x < dayStart || x > mWidth - mPadding) { - return null; - } - // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels - int dayPosition = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); - int day = mFirstJulianDay + dayPosition; - - Time time = new Time(mTimeZone); - if (mWeek == 0) { - // This week is weird... - if (day < Time.EPOCH_JULIAN_DAY) { - day++; - } else if (day == Time.EPOCH_JULIAN_DAY) { - time.set(1, 0, 1970); - time.normalize(true); - return time; - } - } - - time.setJulianDay(day); - return time; - } - - @Override - protected void onDraw(Canvas canvas) { - drawBackground(canvas); - drawWeekNums(canvas); - drawDaySeparators(canvas); - } - - /** - * This draws the selection highlight if a day is selected in this week. - * Override this method if you wish to have a different background drawn. - * - * @param canvas The canvas to draw on - */ - protected void drawBackground(Canvas canvas) { - if (mHasSelectedDay) { - p.setColor(mSelectedWeekBGColor); - p.setStyle(Style.FILL); - } else { - return; - } - r.top = 1; - r.bottom = mHeight - 1; - r.left = mPadding; - r.right = mSelectedLeft; - canvas.drawRect(r, p); - r.left = mSelectedRight; - r.right = mWidth - mPadding; - canvas.drawRect(r, p); - } - - /** - * Draws the week and month day numbers for this week. Override this method - * if you need different placement. - * - * @param canvas The canvas to draw on - */ - protected void drawWeekNums(Canvas canvas) { - int y = ((mHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH; - int nDays = mNumCells; - - int i = 0; - int divisor = 2 * nDays; - if (mShowWeekNum) { - p.setTextSize(MINI_WK_NUMBER_TEXT_SIZE); - p.setStyle(Style.FILL); - p.setTextAlign(Align.CENTER); - p.setAntiAlias(true); - p.setColor(mWeekNumColor); - int x = (mWidth - mPadding * 2) / divisor + mPadding; - canvas.drawText(mDayNumbers[0], x, y, p); - i++; - } - - boolean isFocusMonth = mFocusDay[i]; - mMonthNumPaint.setColor(isFocusMonth ? mFocusMonthColor : mOtherMonthColor); - mMonthNumPaint.setFakeBoldText(false); - for (; i < nDays; i++) { - if (mFocusDay[i] != isFocusMonth) { - isFocusMonth = mFocusDay[i]; - mMonthNumPaint.setColor(isFocusMonth ? mFocusMonthColor : mOtherMonthColor); - } - if (mHasToday && mToday == i) { - mMonthNumPaint.setTextSize(MINI_TODAY_NUMBER_TEXT_SIZE); - mMonthNumPaint.setFakeBoldText(true); - } - int x = (2 * i + 1) * (mWidth - mPadding * 2) / (divisor) + mPadding; - canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint); - if (mHasToday && mToday == i) { - mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); - mMonthNumPaint.setFakeBoldText(false); - } - } - } - - /** - * Draws a horizontal line for separating the weeks. Override this method if - * you want custom separators. - * - * @param canvas The canvas to draw on - */ - protected void drawDaySeparators(Canvas canvas) { - if (mHasSelectedDay) { - r.top = 1; - r.bottom = mHeight - 1; - r.left = mSelectedLeft + 1; - r.right = mSelectedRight - 1; - p.setStrokeWidth(MINI_TODAY_OUTLINE_WIDTH); - p.setStyle(Style.STROKE); - p.setColor(mTodayOutlineColor); - canvas.drawRect(r, p); - } - if (mShowWeekNum) { - p.setColor(mDaySeparatorColor); - p.setStrokeWidth(DAY_SEPARATOR_WIDTH); - - int x = (mWidth - mPadding * 2) / mNumCells + mPadding; - canvas.drawLine(x, 0, x, mHeight, p); - } - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - mWidth = w; - updateSelectionPositions(); - } - - /** - * This calculates the positions for the selected day lines. - */ - protected void updateSelectionPositions() { - if (mHasSelectedDay) { - int selectedPosition = mSelectedDay - mWeekStart; - if (selectedPosition < 0) { - selectedPosition += 7; - } - if (mShowWeekNum) { - selectedPosition++; - } - mSelectedLeft = selectedPosition * (mWidth - mPadding * 2) / mNumCells - + mPadding; - mSelectedRight = (selectedPosition + 1) * (mWidth - mPadding * 2) / mNumCells - + mPadding; - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight); - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - Context context = getContext(); - // only send accessibility events if accessibility and exploration are - // on. - AccessibilityManager am = (AccessibilityManager) context - .getSystemService(Service.ACCESSIBILITY_SERVICE); - if (!am.isEnabled() || !am.isTouchExplorationEnabled()) { - return super.onHoverEvent(event); - } - if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) { - Time hover = getDayFromLocation(event.getX()); - if (hover != null - && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) != 0)) { - Long millis = hover.toMillis(true); - String date = Utils.formatDateRange(context, millis, millis, - DateUtils.FORMAT_SHOW_DATE); - AccessibilityEvent accessEvent = - AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); - accessEvent.getText().add(date); - sendAccessibilityEventUnchecked(accessEvent); - mLastHoverTime = hover; - } - } - return true; - } - - Time mLastHoverTime = null; -}
\ No newline at end of file diff --git a/src/com/android/calendar/month/SimpleWeekView.kt b/src/com/android/calendar/month/SimpleWeekView.kt new file mode 100644 index 00000000..4d1298d4 --- /dev/null +++ b/src/com/android/calendar/month/SimpleWeekView.kt @@ -0,0 +1,563 @@ +/* + * Copyright (C) 2021 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.calendar.month + +import com.android.calendar.R +import com.android.calendar.Utils +import android.app.Service +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Paint.Align +import android.graphics.Paint.Style +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.text.format.DateUtils +import android.text.format.Time +import android.view.MotionEvent +import android.view.View +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityManager +import java.security.InvalidParameterException +import java.util.HashMap + +/** + * + * + * This is a dynamic view for drawing a single week. It can be configured to + * display the week number, start the week on a given day, or show a reduced + * number of days. It is intended for use as a single view within a ListView. + * See [SimpleWeeksAdapter] for usage. + * + */ +open class SimpleWeekView(context: Context) : View(context) { + // affects the padding on the sides of this view + @JvmField protected var mPadding = 0 + @JvmField protected var r: Rect = Rect() + @JvmField protected var p: Paint = Paint() + @JvmField protected var mMonthNumPaint: Paint = Paint() + @JvmField protected var mSelectedDayLine: Drawable + + // Cache the number strings so we don't have to recompute them each time + @JvmField protected var mDayNumbers: Array<String?>? = null + + // How many days to display + @JvmField protected var mNumDays = DEFAULT_NUM_DAYS + + // The number of days + a spot for week number if it is displayed + @JvmField protected var mNumCells = mNumDays + + // Quick lookup for checking which days are in the focus month + @JvmField protected var mFocusDay: BooleanArray = BooleanArray(mNumCells) + + // Quick lookup for checking which days are in an odd month (to set a different background) + @JvmField protected var mOddMonth: BooleanArray = BooleanArray(mNumCells) + + // The Julian day of the first day displayed by this item + @JvmField protected var mFirstJulianDay = -1 + + // The month of the first day in this week + @JvmField protected var firstMonth = -1 + + // The month of the last day in this week + @JvmField protected var lastMonth = -1 + + // The position of this week, equivalent to weeks since the week of Jan 1st, + // 1970 + @JvmField var mWeek = -1 + + // Quick reference to the width of this view, matches parent + @JvmField protected var mWidth = 0 + + // The height this view should draw at in pixels, set by height param + @JvmField protected var mHeight = DEFAULT_HEIGHT + + // Whether the week number should be shown + @JvmField protected var mShowWeekNum = false + + // If this view contains the selected day + @JvmField protected var mHasSelectedDay = false + + // If this view contains the today + open protected var mHasToday = false + + // Which day is selected [0-6] or -1 if no day is selected + @JvmField protected var mSelectedDay = DEFAULT_SELECTED_DAY + + // Which day is today [0-6] or -1 if no day is today + @JvmField protected var mToday: Int = DEFAULT_SELECTED_DAY + + // Which day of the week to start on [0-6] + @JvmField protected var mWeekStart = DEFAULT_WEEK_START + + // The left edge of the selected day + @JvmField protected var mSelectedLeft = -1 + + // The right edge of the selected day + @JvmField protected var mSelectedRight = -1 + + // The timezone to display times/dates in (used for determining when Today + // is) + @JvmField protected var mTimeZone: String = Time.getCurrentTimezone() + @JvmField protected var mBGColor: Int + @JvmField protected var mSelectedWeekBGColor: Int + @JvmField protected var mFocusMonthColor: Int + @JvmField protected var mOtherMonthColor: Int + @JvmField protected var mDaySeparatorColor: Int + @JvmField protected var mTodayOutlineColor: Int + @JvmField protected var mWeekNumColor: Int + + /** + * Sets all the parameters for displaying this week. The only required + * parameter is the week number. Other parameters have a default value and + * will only update if a new value is included, except for focus month, + * which will always default to no focus month if no value is passed in. See + * [.VIEW_PARAMS_HEIGHT] for more info on parameters. + * + * @param params A map of the new parameters, see + * [.VIEW_PARAMS_HEIGHT] + * @param tz The time zone this view should reference times in + */ + open fun setWeekParams(params: HashMap<String?, Int?>, tz: String) { + if (!params.containsKey(VIEW_PARAMS_WEEK)) { + throw InvalidParameterException("You must specify the week number for this view") + } + setTag(params) + mTimeZone = tz + // We keep the current value for any params not present + if (params.containsKey(VIEW_PARAMS_HEIGHT)) { + mHeight = (params.get(VIEW_PARAMS_HEIGHT))!!.toInt() + if (mHeight < MIN_HEIGHT) { + mHeight = MIN_HEIGHT + } + } + if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { + mSelectedDay = (params.get(VIEW_PARAMS_SELECTED_DAY))!!.toInt() + } + mHasSelectedDay = mSelectedDay != -1 + if (params.containsKey(VIEW_PARAMS_NUM_DAYS)) { + mNumDays = (params.get(VIEW_PARAMS_NUM_DAYS))!!.toInt() + } + if (params.containsKey(VIEW_PARAMS_SHOW_WK_NUM)) { + mShowWeekNum = + if (params.get(VIEW_PARAMS_SHOW_WK_NUM) != 0) { + true + } else { + false + } + } + mNumCells = if (mShowWeekNum) mNumDays + 1 else mNumDays + + // Allocate space for caching the day numbers and focus values + mDayNumbers = arrayOfNulls(mNumCells) + mFocusDay = BooleanArray(mNumCells) + mOddMonth = BooleanArray(mNumCells) + mWeek = (params.get(VIEW_PARAMS_WEEK))!!.toInt() + val julianMonday: Int = Utils.getJulianMondayFromWeeksSinceEpoch(mWeek) + val time = Time(tz) + time.setJulianDay(julianMonday) + + // If we're showing the week number calculate it based on Monday + var i = 0 + if (mShowWeekNum) { + mDayNumbers!![0] = Integer.toString(time.getWeekNumber()) + i++ + } + if (params.containsKey(VIEW_PARAMS_WEEK_START)) { + mWeekStart = (params.get(VIEW_PARAMS_WEEK_START))!!.toInt() + } + + // Now adjust our starting day based on the start day of the week + // If the week is set to start on a Saturday the first week will be + // Dec 27th 1969 -Jan 2nd, 1970 + if (time.weekDay !== mWeekStart) { + var diff: Int = time.weekDay - mWeekStart + if (diff < 0) { + diff += 7 + } + time.monthDay -= diff + time.normalize(true) + } + mFirstJulianDay = Time.getJulianDay(time.toMillis(true), time.gmtoff) + firstMonth = time.month + + // Figure out what day today is + val today = Time(tz) + today.setToNow() + mHasToday = false + mToday = -1 + val focusMonth = if (params.containsKey(VIEW_PARAMS_FOCUS_MONTH)) params.get( + VIEW_PARAMS_FOCUS_MONTH + ) else DEFAULT_FOCUS_MONTH + while (i < mNumCells) { + if (time.monthDay === 1) { + firstMonth = time.month + } + mOddMonth!![i] = time.month % 2 === 1 + if (time.month === focusMonth) { + mFocusDay!![i] = true + } else { + mFocusDay!![i] = false + } + if (time.year === today.year && time.yearDay === today.yearDay) { + mHasToday = true + mToday = i + } + mDayNumbers!![i] = Integer.toString(time.monthDay++) + time.normalize(true) + i++ + } + // We do one extra add at the end of the loop, if that pushed us to a + // new month undo it + if (time.monthDay === 1) { + time.monthDay-- + time.normalize(true) + } + lastMonth = time.month + updateSelectionPositions() + } + + /** + * Sets up the text and style properties for painting. Override this if you + * want to use a different paint. + */ + protected open fun initView() { + p.setFakeBoldText(false) + p.setAntiAlias(true) + p.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE.toFloat()) + p.setStyle(Style.FILL) + mMonthNumPaint = Paint() + mMonthNumPaint?.setFakeBoldText(true) + mMonthNumPaint?.setAntiAlias(true) + mMonthNumPaint?.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE.toFloat()) + mMonthNumPaint?.setColor(mFocusMonthColor) + mMonthNumPaint?.setStyle(Style.FILL) + mMonthNumPaint?.setTextAlign(Align.CENTER) + } + + /** + * Returns the month of the first day in this week + * + * @return The month the first day of this view is in + */ + fun getFirstMonth(): Int { + return firstMonth + } + + /** + * Returns the month of the last day in this week + * + * @return The month the last day of this view is in + */ + fun getLastMonth(): Int { + return lastMonth + } + + /** + * Returns the julian day of the first day in this view. + * + * @return The julian day of the first day in the view. + */ + fun getFirstJulianDay(): Int { + return mFirstJulianDay + } + + /** + * Calculates the day that the given x position is in, accounting for week + * number. Returns a Time referencing that day or null if + * + * @param x The x position of the touch event + * @return A time object for the tapped day or null if the position wasn't + * in a day + */ + open fun getDayFromLocation(x: Float): Time? { + val dayStart = + if (mShowWeekNum) (mWidth - mPadding * 2) / mNumCells + mPadding else mPadding + if (x < dayStart || x > mWidth - mPadding) { + return null + } + // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels + val dayPosition = ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)).toInt() + var day = mFirstJulianDay + dayPosition + val time = Time(mTimeZone) + if (mWeek == 0) { + // This week is weird... + if (day < Time.EPOCH_JULIAN_DAY) { + day++ + } else if (day == Time.EPOCH_JULIAN_DAY) { + time.set(1, 0, 1970) + time.normalize(true) + return time + } + } + time.setJulianDay(day) + return time + } + + @Override + protected override fun onDraw(canvas: Canvas) { + drawBackground(canvas) + drawWeekNums(canvas) + drawDaySeparators(canvas) + } + + /** + * This draws the selection highlight if a day is selected in this week. + * Override this method if you wish to have a different background drawn. + * + * @param canvas The canvas to draw on + */ + protected open fun drawBackground(canvas: Canvas) { + if (mHasSelectedDay) { + p.setColor(mSelectedWeekBGColor) + p.setStyle(Style.FILL) + } else { + return + } + r.top = 1 + r.bottom = mHeight - 1 + r.left = mPadding + r.right = mSelectedLeft + canvas.drawRect(r, p) + r.left = mSelectedRight + r.right = mWidth - mPadding + canvas.drawRect(r, p) + } + + /** + * Draws the week and month day numbers for this week. Override this method + * if you need different placement. + * + * @param canvas The canvas to draw on + */ + protected open fun drawWeekNums(canvas: Canvas) { + val y = (mHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH + val nDays = mNumCells + var i = 0 + val divisor = 2 * nDays + if (mShowWeekNum) { + p.setTextSize(MINI_WK_NUMBER_TEXT_SIZE.toFloat()) + p.setStyle(Style.FILL) + p.setTextAlign(Align.CENTER) + p.setAntiAlias(true) + p.setColor(mWeekNumColor) + val x = (mWidth - mPadding * 2) / divisor + mPadding + canvas.drawText(mDayNumbers!![0] as String, x.toFloat(), y.toFloat(), p) + i++ + } + var isFocusMonth = mFocusDay!![i] + mMonthNumPaint?.setColor(if (isFocusMonth) mFocusMonthColor else mOtherMonthColor) + mMonthNumPaint?.setFakeBoldText(false) + while (i < nDays) { + if (mFocusDay!![i] != isFocusMonth) { + isFocusMonth = mFocusDay!![i] + mMonthNumPaint?.setColor(if (isFocusMonth) mFocusMonthColor else mOtherMonthColor) + } + if (mHasToday && mToday == i) { + mMonthNumPaint?.setTextSize(MINI_TODAY_NUMBER_TEXT_SIZE.toFloat()) + mMonthNumPaint?.setFakeBoldText(true) + } + val x = (2 * i + 1) * (mWidth - mPadding * 2) / divisor + mPadding + canvas.drawText(mDayNumbers!![i] as String, x.toFloat(), y.toFloat(), + mMonthNumPaint as Paint) + if (mHasToday && mToday == i) { + mMonthNumPaint?.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE.toFloat()) + mMonthNumPaint?.setFakeBoldText(false) + } + i++ + } + } + + /** + * Draws a horizontal line for separating the weeks. Override this method if + * you want custom separators. + * + * @param canvas The canvas to draw on + */ + protected open fun drawDaySeparators(canvas: Canvas) { + if (mHasSelectedDay) { + r.top = 1 + r.bottom = mHeight - 1 + r.left = mSelectedLeft + 1 + r.right = mSelectedRight - 1 + p.setStrokeWidth(MINI_TODAY_OUTLINE_WIDTH.toFloat()) + p.setStyle(Style.STROKE) + p.setColor(mTodayOutlineColor) + canvas.drawRect(r, p) + } + if (mShowWeekNum) { + p.setColor(mDaySeparatorColor) + p.setStrokeWidth(DAY_SEPARATOR_WIDTH.toFloat()) + val x = (mWidth - mPadding * 2) / mNumCells + mPadding + canvas.drawLine(x.toFloat(), 0f, x.toFloat(), mHeight.toFloat(), p) + } + } + + @Override + protected override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + mWidth = w + updateSelectionPositions() + } + + /** + * This calculates the positions for the selected day lines. + */ + protected open fun updateSelectionPositions() { + if (mHasSelectedDay) { + var selectedPosition = mSelectedDay - mWeekStart + if (selectedPosition < 0) { + selectedPosition += 7 + } + if (mShowWeekNum) { + selectedPosition++ + } + mSelectedLeft = (selectedPosition * (mWidth - mPadding * 2) / mNumCells + + mPadding) + mSelectedRight = ((selectedPosition + 1) * (mWidth - mPadding * 2) / mNumCells + + mPadding) + } + } + + @Override + protected override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight) + } + + @Override + override fun onHoverEvent(event: MotionEvent): Boolean { + val context: Context = getContext() + // only send accessibility events if accessibility and exploration are + // on. + val am: AccessibilityManager = context + .getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager + if (!am.isEnabled() || !am.isTouchExplorationEnabled()) { + return super.onHoverEvent(event) + } + if (event.getAction() !== MotionEvent.ACTION_HOVER_EXIT) { + val hover: Time? = getDayFromLocation(event.getX()) + if (hover != null && + (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) !== 0) + ) { + val millis: Long = hover.toMillis(true) + val date: String? = Utils.formatDateRange( + context, millis, millis, + DateUtils.FORMAT_SHOW_DATE + ) + val accessEvent: AccessibilityEvent = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) + accessEvent.getText().add(date) + sendAccessibilityEventUnchecked(accessEvent) + mLastHoverTime = hover + } + } + return true + } + + @JvmField var mLastHoverTime: Time? = null + + companion object { + private const val TAG = "MonthView" + /** + * These params can be passed into the view to control how it appears. + * [.VIEW_PARAMS_WEEK] is the only required field, though the default + * values are unlikely to fit most layouts correctly. + */ + /** + * This sets the height of this week in pixels + */ + const val VIEW_PARAMS_HEIGHT = "height" + + /** + * This specifies the position (or weeks since the epoch) of this week, + * calculated using [Utils.getWeeksSinceEpochFromJulianDay] + */ + const val VIEW_PARAMS_WEEK = "week" + + /** + * This sets one of the days in this view as selected [Time.SUNDAY] + * through [Time.SATURDAY]. + */ + const val VIEW_PARAMS_SELECTED_DAY = "selected_day" + + /** + * Which day the week should start on. [Time.SUNDAY] through + * [Time.SATURDAY]. + */ + const val VIEW_PARAMS_WEEK_START = "week_start" + + /** + * How many days to display at a time. Days will be displayed starting with + * [.mWeekStart]. + */ + const val VIEW_PARAMS_NUM_DAYS = "num_days" + + /** + * Which month is currently in focus, as defined by [Time.month] + * [0-11]. + */ + const val VIEW_PARAMS_FOCUS_MONTH = "focus_month" + + /** + * If this month should display week numbers. false if 0, true otherwise. + */ + const val VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num" + protected var DEFAULT_HEIGHT = 32 + protected var MIN_HEIGHT = 10 + protected const val DEFAULT_SELECTED_DAY = -1 + protected val DEFAULT_WEEK_START: Int = Time.SUNDAY + protected const val DEFAULT_NUM_DAYS = 7 + protected const val DEFAULT_SHOW_WK_NUM = 0 + protected const val DEFAULT_FOCUS_MONTH = -1 + protected var DAY_SEPARATOR_WIDTH = 1 + protected var MINI_DAY_NUMBER_TEXT_SIZE = 14 + protected var MINI_WK_NUMBER_TEXT_SIZE = 12 + protected var MINI_TODAY_NUMBER_TEXT_SIZE = 18 + protected var MINI_TODAY_OUTLINE_WIDTH = 2 + protected var WEEK_NUM_MARGIN_BOTTOM = 4 + + // used for scaling to the device density + @JvmStatic protected var mScale = 0f + } + + init { + val res: Resources = context.getResources() + mBGColor = res.getColor(R.color.month_bgcolor) + mSelectedWeekBGColor = res.getColor(R.color.month_selected_week_bgcolor) + mFocusMonthColor = res.getColor(R.color.month_mini_day_number) + mOtherMonthColor = res.getColor(R.color.month_other_month_day_number) + mDaySeparatorColor = res.getColor(R.color.month_grid_lines) + mTodayOutlineColor = res.getColor(R.color.mini_month_today_outline_color) + mWeekNumColor = res.getColor(R.color.month_week_num_color) + mSelectedDayLine = res.getDrawable(R.drawable.dayline_minical_holo_light) + if (mScale == 0f) { + mScale = context.getResources().getDisplayMetrics().density + if (mScale != 1f) { + DEFAULT_HEIGHT *= mScale.toInt() + MIN_HEIGHT *= mScale.toInt() + MINI_DAY_NUMBER_TEXT_SIZE *= mScale.toInt() + MINI_TODAY_NUMBER_TEXT_SIZE *= mScale.toInt() + MINI_TODAY_OUTLINE_WIDTH *= mScale.toInt() + WEEK_NUM_MARGIN_BOTTOM *= mScale.toInt() + DAY_SEPARATOR_WIDTH *= mScale.toInt() + MINI_WK_NUMBER_TEXT_SIZE *= mScale.toInt() + } + } + + // Sets up any standard paints that will be used + initView() + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/month/SimpleWeeksAdapter.java b/src/com/android/calendar/month/SimpleWeeksAdapter.java deleted file mode 100644 index d29b2622..00000000 --- a/src/com/android/calendar/month/SimpleWeeksAdapter.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar.month; - -// TODO Remove calendar imports when the required methods have been -// refactored into the public api -import com.android.calendar.CalendarController; -import com.android.calendar.Utils; - -import android.content.Context; -import android.text.format.Time; -import android.util.Log; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnTouchListener; -import android.view.ViewGroup; -import android.widget.AbsListView.LayoutParams; -import android.widget.BaseAdapter; -import android.widget.ListView; - -import java.util.Calendar; -import java.util.HashMap; -import java.util.Locale; - -/** - * <p> - * This is a specialized adapter for creating a list of weeks with selectable - * days. It can be configured to display the week number, start the week on a - * given day, show a reduced number of days, or display an arbitrary number of - * weeks at a time. See {@link SimpleDayPickerFragment} for usage. - * </p> - */ -public class SimpleWeeksAdapter extends BaseAdapter implements OnTouchListener { - - private static final String TAG = "MonthByWeek"; - - /** - * The number of weeks to display at a time. - */ - public static final String WEEK_PARAMS_NUM_WEEKS = "num_weeks"; - /** - * Which month should be in focus currently. - */ - public static final String WEEK_PARAMS_FOCUS_MONTH = "focus_month"; - /** - * Whether the week number should be shown. Non-zero to show them. - */ - public static final String WEEK_PARAMS_SHOW_WEEK = "week_numbers"; - /** - * Which day the week should start on. {@link Time#SUNDAY} through - * {@link Time#SATURDAY}. - */ - public static final String WEEK_PARAMS_WEEK_START = "week_start"; - /** - * The Julian day to highlight as selected. - */ - public static final String WEEK_PARAMS_JULIAN_DAY = "selected_day"; - /** - * How many days of the week to display [1-7]. - */ - public static final String WEEK_PARAMS_DAYS_PER_WEEK = "days_per_week"; - - protected static final int WEEK_COUNT = CalendarController.MAX_CALENDAR_WEEK - - CalendarController.MIN_CALENDAR_WEEK; - protected static int DEFAULT_NUM_WEEKS = 6; - protected static int DEFAULT_MONTH_FOCUS = 0; - protected static int DEFAULT_DAYS_PER_WEEK = 7; - protected static int DEFAULT_WEEK_HEIGHT = 32; - protected static int WEEK_7_OVERHANG_HEIGHT = 7; - - protected static float mScale = 0; - protected Context mContext; - // The day to highlight as selected - protected Time mSelectedDay; - // The week since 1970 that the selected day is in - protected int mSelectedWeek; - // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0). - protected int mFirstDayOfWeek; - protected boolean mShowWeekNumber = false; - protected GestureDetector mGestureDetector; - protected int mNumWeeks = DEFAULT_NUM_WEEKS; - protected int mDaysPerWeek = DEFAULT_DAYS_PER_WEEK; - protected int mFocusMonth = DEFAULT_MONTH_FOCUS; - - public SimpleWeeksAdapter(Context context, HashMap<String, Integer> params) { - mContext = context; - - // Get default week start based on locale, subtracting one for use with android Time. - Calendar cal = Calendar.getInstance(Locale.getDefault()); - mFirstDayOfWeek = cal.getFirstDayOfWeek() - 1; - - if (mScale == 0) { - mScale = context.getResources().getDisplayMetrics().density; - if (mScale != 1) { - WEEK_7_OVERHANG_HEIGHT *= mScale; - } - } - init(); - updateParams(params); - } - - /** - * Set up the gesture detector and selected time - */ - protected void init() { - mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); - mSelectedDay = new Time(); - mSelectedDay.setToNow(); - } - - /** - * Parse the parameters and set any necessary fields. See - * {@link #WEEK_PARAMS_NUM_WEEKS} for parameter details. - * - * @param params A list of parameters for this adapter - */ - public void updateParams(HashMap<String, Integer> params) { - if (params == null) { - Log.e(TAG, "WeekParameters are null! Cannot update adapter."); - return; - } - if (params.containsKey(WEEK_PARAMS_FOCUS_MONTH)) { - mFocusMonth = params.get(WEEK_PARAMS_FOCUS_MONTH); - } - if (params.containsKey(WEEK_PARAMS_FOCUS_MONTH)) { - mNumWeeks = params.get(WEEK_PARAMS_NUM_WEEKS); - } - if (params.containsKey(WEEK_PARAMS_SHOW_WEEK)) { - mShowWeekNumber = params.get(WEEK_PARAMS_SHOW_WEEK) != 0; - } - if (params.containsKey(WEEK_PARAMS_WEEK_START)) { - mFirstDayOfWeek = params.get(WEEK_PARAMS_WEEK_START); - } - if (params.containsKey(WEEK_PARAMS_JULIAN_DAY)) { - int julianDay = params.get(WEEK_PARAMS_JULIAN_DAY); - mSelectedDay.setJulianDay(julianDay); - mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay(julianDay, mFirstDayOfWeek); - } - if (params.containsKey(WEEK_PARAMS_DAYS_PER_WEEK)) { - mDaysPerWeek = params.get(WEEK_PARAMS_DAYS_PER_WEEK); - } - refresh(); - } - - /** - * Updates the selected day and related parameters. - * - * @param selectedTime The time to highlight - */ - public void setSelectedDay(Time selectedTime) { - mSelectedDay.set(selectedTime); - long millis = mSelectedDay.normalize(true); - mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay( - Time.getJulianDay(millis, mSelectedDay.gmtoff), mFirstDayOfWeek); - notifyDataSetChanged(); - } - - /** - * Returns the currently highlighted day - * - * @return - */ - public Time getSelectedDay() { - return mSelectedDay; - } - - /** - * updates any config options that may have changed and refreshes the view - */ - protected void refresh() { - notifyDataSetChanged(); - } - - @Override - public int getCount() { - return WEEK_COUNT; - } - - @Override - public Object getItem(int position) { - return null; - } - - @Override - public long getItemId(int position) { - return position; - } - - @SuppressWarnings("unchecked") - @Override - public View getView(int position, View convertView, ViewGroup parent) { - SimpleWeekView v; - HashMap<String, Integer> drawingParams = null; - if (convertView != null) { - v = (SimpleWeekView) convertView; - // We store the drawing parameters in the view so it can be recycled - drawingParams = (HashMap<String, Integer>) v.getTag(); - } else { - v = new SimpleWeekView(mContext); - // Set up the new view - LayoutParams params = new LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - v.setLayoutParams(params); - v.setClickable(true); - v.setOnTouchListener(this); - } - if (drawingParams == null) { - drawingParams = new HashMap<String, Integer>(); - } - drawingParams.clear(); - - int selectedDay = -1; - if (mSelectedWeek == position) { - selectedDay = mSelectedDay.weekDay; - } - - // pass in all the view parameters - drawingParams.put(SimpleWeekView.VIEW_PARAMS_HEIGHT, - (parent.getHeight() - WEEK_7_OVERHANG_HEIGHT) / mNumWeeks); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, mShowWeekNumber ? 1 : 0); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position); - drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth); - v.setWeekParams(drawingParams, mSelectedDay.timezone); - v.invalidate(); - - return v; - } - - /** - * Changes which month is in focus and updates the view. - * - * @param month The month to show as in focus [0-11] - */ - public void updateFocusMonth(int month) { - mFocusMonth = month; - notifyDataSetChanged(); - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (mGestureDetector.onTouchEvent(event)) { - SimpleWeekView view = (SimpleWeekView) v; - Time day = ((SimpleWeekView)v).getDayFromLocation(event.getX()); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Touched day at Row=" + view.mWeek + " day=" + day.toString()); - } - if (day != null) { - onDayTapped(day); - } - return true; - } - return false; - } - - /** - * Maintains the same hour/min/sec but moves the day to the tapped day. - * - * @param day The day that was tapped - */ - protected void onDayTapped(Time day) { - day.hour = mSelectedDay.hour; - day.minute = mSelectedDay.minute; - day.second = mSelectedDay.second; - setSelectedDay(day); - } - - - /** - * This is here so we can identify single tap events and set the selected - * day correctly - */ - protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { - @Override - public boolean onSingleTapUp(MotionEvent e) { - return true; - } - } - - ListView mListView; - - public void setListView(ListView lv) { - mListView = lv; - } -} diff --git a/src/com/android/calendar/month/SimpleWeeksAdapter.kt b/src/com/android/calendar/month/SimpleWeeksAdapter.kt new file mode 100644 index 00000000..164f05c5 --- /dev/null +++ b/src/com/android/calendar/month/SimpleWeeksAdapter.kt @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2021 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.calendar.month +// TODO Remove calendar imports when the required methods have been +// refactored into the public api +import com.android.calendar.CalendarController +import com.android.calendar.Utils +import android.content.Context +import android.text.format.Time +import android.util.Log +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.view.View.OnTouchListener +import android.view.ViewGroup +import android.widget.AbsListView.LayoutParams +import android.widget.BaseAdapter +import android.widget.ListView +import java.util.Calendar +import java.util.HashMap +import java.util.Locale + +/** + * + * + * This is a specialized adapter for creating a list of weeks with selectable + * days. It can be configured to display the week number, start the week on a + * given day, show a reduced number of days, or display an arbitrary number of + * weeks at a time. See [SimpleDayPickerFragment] for usage. + * + */ +open class SimpleWeeksAdapter(context: Context, params: HashMap<String?, Int?>?) : BaseAdapter(), + OnTouchListener { + protected var mContext: Context + + // The day to highlight as selected + protected var mSelectedDay: Time? = null + + // The week since 1970 that the selected day is in + protected var mSelectedWeek = 0 + + // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0). + protected var mFirstDayOfWeek: Int + protected var mShowWeekNumber = false + protected var mGestureDetector: GestureDetector? = null + protected var mNumWeeks = DEFAULT_NUM_WEEKS + protected var mDaysPerWeek = DEFAULT_DAYS_PER_WEEK + protected var mFocusMonth = DEFAULT_MONTH_FOCUS + + /** + * Set up the gesture detector and selected time + */ + protected open fun init() { + mGestureDetector = GestureDetector(mContext, CalendarGestureListener()) + mSelectedDay = Time() + mSelectedDay?.setToNow() + } + + /** + * Parse the parameters and set any necessary fields. See + * [.WEEK_PARAMS_NUM_WEEKS] for parameter details. + * + * @param params A list of parameters for this adapter + */ + fun updateParams(params: HashMap<String?, Int?>?) { + if (params == null) { + Log.e(TAG, "WeekParameters are null! Cannot update adapter.") + return + } + if (params.containsKey(WEEK_PARAMS_FOCUS_MONTH)) { + // Casting from Int? --> Int + mFocusMonth = params.get(WEEK_PARAMS_FOCUS_MONTH) as Int + } + if (params.containsKey(WEEK_PARAMS_FOCUS_MONTH)) { + // Casting from Int? --> Int + mNumWeeks = params.get(WEEK_PARAMS_NUM_WEEKS) as Int + } + if (params.containsKey(WEEK_PARAMS_SHOW_WEEK)) { + // Casting from Int? --> Int + mShowWeekNumber = params.get(WEEK_PARAMS_SHOW_WEEK) as Int != 0 + } + if (params.containsKey(WEEK_PARAMS_WEEK_START)) { + // Casting from Int? --> Int + mFirstDayOfWeek = params.get(WEEK_PARAMS_WEEK_START) as Int + } + if (params.containsKey(WEEK_PARAMS_JULIAN_DAY)) { + // Casting from Int? --> Int + val julianDay: Int = params.get(WEEK_PARAMS_JULIAN_DAY) as Int + mSelectedDay?.setJulianDay(julianDay) + mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay(julianDay, mFirstDayOfWeek) + } + if (params.containsKey(WEEK_PARAMS_DAYS_PER_WEEK)) { + // Casting from Int? --> Int + mDaysPerWeek = params.get(WEEK_PARAMS_DAYS_PER_WEEK) as Int + } + refresh() + } + + /** + * Updates the selected day and related parameters. + * + * @param selectedTime The time to highlight + */ + open fun setSelectedDay(selectedTime: Time?) { + mSelectedDay?.set(selectedTime) + val millis: Long = mSelectedDay!!.normalize(true) + mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay( + Time.getJulianDay(millis, mSelectedDay!!.gmtoff), mFirstDayOfWeek + ) + notifyDataSetChanged() + } + + /** + * Returns the currently highlighted day + * + * @return + */ + fun getSelectedDay(): Time? { + return mSelectedDay + } + + /** + * updates any config options that may have changed and refreshes the view + */ + internal open fun refresh() { + notifyDataSetChanged() + } + + @Override + override fun getCount(): Int { + return WEEK_COUNT + } + + @Override + override fun getItem(position: Int): Any? { + return null + } + + @Override + override fun getItemId(position: Int): Long { + return position.toLong() + } + + @SuppressWarnings("unchecked") + @Override + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val v: SimpleWeekView + var drawingParams: HashMap<String?, Int?>? = null + if (convertView != null) { + v = convertView as SimpleWeekView + // We store the drawing parameters in the view so it can be recycled + drawingParams = v.getTag() as HashMap<String?, Int?> + } else { + v = SimpleWeekView(mContext) + // Set up the new view + val params = LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT + ) + v.setLayoutParams(params) + v.setClickable(true) + v.setOnTouchListener(this) + } + if (drawingParams == null) { + drawingParams = HashMap<String?, Int?>() + } + drawingParams.clear() + var selectedDay = -1 + if (mSelectedWeek == position) { + selectedDay = mSelectedDay!!.weekDay + } + + // pass in all the view parameters + drawingParams.put( + SimpleWeekView.VIEW_PARAMS_HEIGHT, + (parent.getHeight() - WEEK_7_OVERHANG_HEIGHT) / mNumWeeks + ) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, if (mShowWeekNumber) 1 else 0) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position) + drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth) + v.setWeekParams(drawingParams, mSelectedDay!!.timezone) + v.invalidate() + return v + } + + /** + * Changes which month is in focus and updates the view. + * + * @param month The month to show as in focus [0-11] + */ + fun updateFocusMonth(month: Int) { + mFocusMonth = month + notifyDataSetChanged() + } + + @Override + override fun onTouch(v: View, event: MotionEvent): Boolean { + if (mGestureDetector!!.onTouchEvent(event)) { + val view: SimpleWeekView = v as SimpleWeekView + val day: Time? = (v as SimpleWeekView).getDayFromLocation(event.getX()) + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Touched day at Row=" + view.mWeek.toString() + " day=" + + day?.toString()) + } + if (day != null) { + onDayTapped(day) + } + return true + } + return false + } + + /** + * Maintains the same hour/min/sec but moves the day to the tapped day. + * + * @param day The day that was tapped + */ + protected open fun onDayTapped(day: Time) { + day.hour = mSelectedDay!!.hour + day.minute = mSelectedDay!!.minute + day.second = mSelectedDay!!.second + setSelectedDay(day) + } + + /** + * This is here so we can identify single tap events and set the selected + * day correctly + */ + protected inner class CalendarGestureListener : GestureDetector.SimpleOnGestureListener() { + @Override + override fun onSingleTapUp(e: MotionEvent): Boolean { + return true + } + } + + var mListView: ListView? = null + fun setListView(lv: ListView?) { + mListView = lv + } + + companion object { + private const val TAG = "MonthByWeek" + + /** + * The number of weeks to display at a time. + */ + const val WEEK_PARAMS_NUM_WEEKS = "num_weeks" + + /** + * Which month should be in focus currently. + */ + const val WEEK_PARAMS_FOCUS_MONTH = "focus_month" + + /** + * Whether the week number should be shown. Non-zero to show them. + */ + const val WEEK_PARAMS_SHOW_WEEK = "week_numbers" + + /** + * Which day the week should start on. [Time.SUNDAY] through + * [Time.SATURDAY]. + */ + const val WEEK_PARAMS_WEEK_START = "week_start" + + /** + * The Julian day to highlight as selected. + */ + const val WEEK_PARAMS_JULIAN_DAY = "selected_day" + + /** + * How many days of the week to display [1-7]. + */ + const val WEEK_PARAMS_DAYS_PER_WEEK = "days_per_week" + protected const val WEEK_COUNT = CalendarController.MAX_CALENDAR_WEEK - + CalendarController.MIN_CALENDAR_WEEK + protected var DEFAULT_NUM_WEEKS = 6 + protected var DEFAULT_MONTH_FOCUS = 0 + protected var DEFAULT_DAYS_PER_WEEK = 7 + protected var DEFAULT_WEEK_HEIGHT = 32 + protected var WEEK_7_OVERHANG_HEIGHT = 7 + protected var mScale = 0f + } + + init { + mContext = context + + // Get default week start based on locale, subtracting one for use with android Time. + val cal: Calendar = Calendar.getInstance(Locale.getDefault()) + mFirstDayOfWeek = cal.getFirstDayOfWeek() - 1 + if (mScale == 0f) { + mScale = context.getResources().getDisplayMetrics().density + if (mScale != 1f) { + WEEK_7_OVERHANG_HEIGHT *= mScale.toInt() + } + } + init() + updateParams(params) + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/widget/CalendarAppWidgetModel.java b/src/com/android/calendar/widget/CalendarAppWidgetModel.java deleted file mode 100644 index a989e18b..00000000 --- a/src/com/android/calendar/widget/CalendarAppWidgetModel.java +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright (C) 2010 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.calendar.widget; - -import com.android.calendar.R; -import com.android.calendar.Utils; - -import android.content.Context; -import android.database.Cursor; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; -import android.view.View; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.TimeZone; - -class CalendarAppWidgetModel { - private static final String TAG = CalendarAppWidgetModel.class.getSimpleName(); - private static final boolean LOGD = false; - - private String mHomeTZName; - private boolean mShowTZ; - /** - * {@link RowInfo} is a class that represents a single row in the widget. It - * is actually only a pointer to either a {@link DayInfo} or an - * {@link EventInfo} instance, since a row in the widget might be either a - * day header or an event. - */ - static class RowInfo { - static final int TYPE_DAY = 0; - static final int TYPE_MEETING = 1; - - /** - * mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING) - */ - final int mType; - - /** - * If mType is TYPE_DAY, then mData is the index into day infos. - * Otherwise mType is TYPE_MEETING and mData is the index into event - * infos. - */ - final int mIndex; - - RowInfo(int type, int index) { - mType = type; - mIndex = index; - } - } - - /** - * {@link EventInfo} is a class that represents an event in the widget. It - * contains all of the data necessary to display that event, including the - * properly localized strings and visibility settings. - */ - static class EventInfo { - int visibWhen; // Visibility value for When textview (View.GONE or View.VISIBLE) - String when; - int visibWhere; // Visibility value for Where textview (View.GONE or View.VISIBLE) - String where; - int visibTitle; // Visibility value for Title textview (View.GONE or View.VISIBLE) - String title; - int selfAttendeeStatus; - - long id; - long start; - long end; - boolean allDay; - int color; - - public EventInfo() { - visibWhen = View.GONE; - visibWhere = View.GONE; - visibTitle = View.GONE; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("EventInfo [visibTitle="); - builder.append(visibTitle); - builder.append(", title="); - builder.append(title); - builder.append(", visibWhen="); - builder.append(visibWhen); - builder.append(", id="); - builder.append(id); - builder.append(", when="); - builder.append(when); - builder.append(", visibWhere="); - builder.append(visibWhere); - builder.append(", where="); - builder.append(where); - builder.append(", color="); - builder.append(String.format("0x%x", color)); - builder.append(", selfAttendeeStatus="); - builder.append(selfAttendeeStatus); - builder.append("]"); - return builder.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (allDay ? 1231 : 1237); - result = prime * result + (int) (id ^ (id >>> 32)); - result = prime * result + (int) (end ^ (end >>> 32)); - result = prime * result + (int) (start ^ (start >>> 32)); - result = prime * result + ((title == null) ? 0 : title.hashCode()); - result = prime * result + visibTitle; - result = prime * result + visibWhen; - result = prime * result + visibWhere; - result = prime * result + ((when == null) ? 0 : when.hashCode()); - result = prime * result + ((where == null) ? 0 : where.hashCode()); - result = prime * result + color; - result = prime * result + selfAttendeeStatus; - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - EventInfo other = (EventInfo) obj; - if (id != other.id) - return false; - if (allDay != other.allDay) - return false; - if (end != other.end) - return false; - if (start != other.start) - return false; - if (title == null) { - if (other.title != null) - return false; - } else if (!title.equals(other.title)) - return false; - if (visibTitle != other.visibTitle) - return false; - if (visibWhen != other.visibWhen) - return false; - if (visibWhere != other.visibWhere) - return false; - if (when == null) { - if (other.when != null) - return false; - } else if (!when.equals(other.when)) { - return false; - } - if (where == null) { - if (other.where != null) - return false; - } else if (!where.equals(other.where)) { - return false; - } - if (color != other.color) { - return false; - } - if (selfAttendeeStatus != other.selfAttendeeStatus) { - return false; - } - return true; - } - } - - /** - * {@link DayInfo} is a class that represents a day header in the widget. It - * contains all of the data necessary to display that day header, including - * the properly localized string. - */ - static class DayInfo { - - /** The Julian day */ - final int mJulianDay; - - /** The string representation of this day header, to be displayed */ - final String mDayLabel; - - DayInfo(int julianDay, String label) { - mJulianDay = julianDay; - mDayLabel = label; - } - - @Override - public String toString() { - return mDayLabel; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mDayLabel == null) ? 0 : mDayLabel.hashCode()); - result = prime * result + mJulianDay; - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - DayInfo other = (DayInfo) obj; - if (mDayLabel == null) { - if (other.mDayLabel != null) - return false; - } else if (!mDayLabel.equals(other.mDayLabel)) - return false; - if (mJulianDay != other.mJulianDay) - return false; - return true; - } - - } - - final List<RowInfo> mRowInfos; - final List<EventInfo> mEventInfos; - final List<DayInfo> mDayInfos; - final Context mContext; - final long mNow; - final int mTodayJulianDay; - final int mMaxJulianDay; - - public CalendarAppWidgetModel(Context context, String timeZone) { - mNow = System.currentTimeMillis(); - Time time = new Time(timeZone); - time.setToNow(); // This is needed for gmtoff to be set - mTodayJulianDay = Time.getJulianDay(mNow, time.gmtoff); - mMaxJulianDay = mTodayJulianDay + CalendarAppWidgetService.MAX_DAYS - 1; - mEventInfos = new ArrayList<EventInfo>(50); - mRowInfos = new ArrayList<RowInfo>(50); - mDayInfos = new ArrayList<DayInfo>(8); - mContext = context; - } - - public void buildFromCursor(Cursor cursor, String timeZone) { - final Time recycle = new Time(timeZone); - final ArrayList<LinkedList<RowInfo>> mBuckets = - new ArrayList<LinkedList<RowInfo>>(CalendarAppWidgetService.MAX_DAYS); - for (int i = 0; i < CalendarAppWidgetService.MAX_DAYS; i++) { - mBuckets.add(new LinkedList<RowInfo>()); - } - recycle.setToNow(); - mShowTZ = !TextUtils.equals(timeZone, Time.getCurrentTimezone()); - if (mShowTZ) { - mHomeTZName = TimeZone.getTimeZone(timeZone).getDisplayName(recycle.isDst != 0, - TimeZone.SHORT); - } - - cursor.moveToPosition(-1); - String tz = Utils.getTimeZone(mContext, null); - while (cursor.moveToNext()) { - final int rowId = cursor.getPosition(); - final long eventId = cursor.getLong(CalendarAppWidgetService.INDEX_EVENT_ID); - final boolean allDay = cursor.getInt(CalendarAppWidgetService.INDEX_ALL_DAY) != 0; - long start = cursor.getLong(CalendarAppWidgetService.INDEX_BEGIN); - long end = cursor.getLong(CalendarAppWidgetService.INDEX_END); - final String title = cursor.getString(CalendarAppWidgetService.INDEX_TITLE); - final String location = - cursor.getString(CalendarAppWidgetService.INDEX_EVENT_LOCATION); - // we don't compute these ourselves because it seems to produce the - // wrong endDay for all day events - final int startDay = cursor.getInt(CalendarAppWidgetService.INDEX_START_DAY); - final int endDay = cursor.getInt(CalendarAppWidgetService.INDEX_END_DAY); - final int color = cursor.getInt(CalendarAppWidgetService.INDEX_COLOR); - final int selfStatus = cursor - .getInt(CalendarAppWidgetService.INDEX_SELF_ATTENDEE_STATUS); - - // Adjust all-day times into local timezone - if (allDay) { - start = Utils.convertAlldayUtcToLocal(recycle, start, tz); - end = Utils.convertAlldayUtcToLocal(recycle, end, tz); - } - - if (LOGD) { - Log.d(TAG, "Row #" + rowId + " allDay:" + allDay + " start:" + start - + " end:" + end + " eventId:" + eventId); - } - - // we might get some extra events when querying, in order to - // deal with all-day events - if (end < mNow) { - continue; - } - - int i = mEventInfos.size(); - mEventInfos.add(populateEventInfo(eventId, allDay, start, end, startDay, endDay, title, - location, color, selfStatus)); - // populate the day buckets that this event falls into - int from = Math.max(startDay, mTodayJulianDay); - int to = Math.min(endDay, mMaxJulianDay); - for (int day = from; day <= to; day++) { - LinkedList<RowInfo> bucket = mBuckets.get(day - mTodayJulianDay); - RowInfo rowInfo = new RowInfo(RowInfo.TYPE_MEETING, i); - if (allDay) { - bucket.addFirst(rowInfo); - } else { - bucket.add(rowInfo); - } - } - } - - int day = mTodayJulianDay; - int count = 0; - for (LinkedList<RowInfo> bucket : mBuckets) { - if (!bucket.isEmpty()) { - // We don't show day header in today - if (day != mTodayJulianDay) { - final DayInfo dayInfo = populateDayInfo(day, recycle); - // Add the day header - final int dayIndex = mDayInfos.size(); - mDayInfos.add(dayInfo); - mRowInfos.add(new RowInfo(RowInfo.TYPE_DAY, dayIndex)); - } - - // Add the event row infos - mRowInfos.addAll(bucket); - count += bucket.size(); - } - day++; - if (count >= CalendarAppWidgetService.EVENT_MIN_COUNT) { - break; - } - } - } - - private EventInfo populateEventInfo(long eventId, boolean allDay, long start, long end, - int startDay, int endDay, String title, String location, int color, int selfStatus) { - EventInfo eventInfo = new EventInfo(); - - // Compute a human-readable string for the start time of the event - StringBuilder whenString = new StringBuilder(); - int visibWhen; - int flags = DateUtils.FORMAT_ABBREV_ALL; - visibWhen = View.VISIBLE; - if (allDay) { - flags |= DateUtils.FORMAT_SHOW_DATE; - whenString.append(Utils.formatDateRange(mContext, start, end, flags)); - } else { - flags |= DateUtils.FORMAT_SHOW_TIME; - if (DateFormat.is24HourFormat(mContext)) { - flags |= DateUtils.FORMAT_24HOUR; - } - if (endDay > startDay) { - flags |= DateUtils.FORMAT_SHOW_DATE; - } - whenString.append(Utils.formatDateRange(mContext, start, end, flags)); - - if (mShowTZ) { - whenString.append(" ").append(mHomeTZName); - } - } - eventInfo.id = eventId; - eventInfo.start = start; - eventInfo.end = end; - eventInfo.allDay = allDay; - eventInfo.when = whenString.toString(); - eventInfo.visibWhen = visibWhen; - eventInfo.color = color; - eventInfo.selfAttendeeStatus = selfStatus; - - // What - if (TextUtils.isEmpty(title)) { - eventInfo.title = mContext.getString(R.string.no_title_label); - } else { - eventInfo.title = title; - } - eventInfo.visibTitle = View.VISIBLE; - - // Where - if (!TextUtils.isEmpty(location)) { - eventInfo.visibWhere = View.VISIBLE; - eventInfo.where = location; - } else { - eventInfo.visibWhere = View.GONE; - } - return eventInfo; - } - - private DayInfo populateDayInfo(int julianDay, Time recycle) { - long millis = recycle.setJulianDay(julianDay); - int flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE; - - String label; - if (julianDay == mTodayJulianDay + 1) { - label = mContext.getString(R.string.agenda_tomorrow, - Utils.formatDateRange(mContext, millis, millis, flags).toString()); - } else { - flags |= DateUtils.FORMAT_SHOW_WEEKDAY; - label = Utils.formatDateRange(mContext, millis, millis, flags); - } - return new DayInfo(julianDay, label); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("\nCalendarAppWidgetModel [eventInfos="); - builder.append(mEventInfos); - builder.append("]"); - return builder.toString(); - } -}
\ No newline at end of file diff --git a/src/com/android/calendar/widget/CalendarAppWidgetModel.kt b/src/com/android/calendar/widget/CalendarAppWidgetModel.kt new file mode 100644 index 00000000..440d178b --- /dev/null +++ b/src/com/android/calendar/widget/CalendarAppWidgetModel.kt @@ -0,0 +1,409 @@ +/* + * Copyright (C) 2021 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.calendar.widget + +import com.android.calendar.R +import com.android.calendar.Utils +import android.content.Context +import android.database.Cursor +import android.text.TextUtils +import android.text.format.DateFormat +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log +import android.view.View +import java.util.ArrayList +import java.util.LinkedList +import java.util.TimeZone + +internal class CalendarAppWidgetModel(context: Context, timeZone: String?) { + private var mHomeTZName: String? = null + private var mShowTZ = false + + /** + * [RowInfo] is a class that represents a single row in the widget. It + * is actually only a pointer to either a [DayInfo] or an + * [EventInfo] instance, since a row in the widget might be either a + * day header or an event. + */ + internal class RowInfo( + /** + * mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING) + */ + @JvmField val mType: Int, + /** + * If mType is TYPE_DAY, then mData is the index into day infos. + * Otherwise mType is TYPE_MEETING and mData is the index into event + * infos. + */ + @JvmField val mIndex: Int + ) { + companion object { + const val TYPE_DAY = 0 + const val TYPE_MEETING = 1 + } + } + + /** + * [EventInfo] is a class that represents an event in the widget. It + * contains all of the data necessary to display that event, including the + * properly localized strings and visibility settings. + */ + internal class EventInfo { + // Visibility value for When textview (View.GONE or View.VISIBLE) + @JvmField var visibWhen: Int + @JvmField var `when`: String? = null + // Visibility value for Where textview (View.GONE or View.VISIBLE) + @JvmField var visibWhere: Int + @JvmField var where: String? = null + // Visibility value for Title textview (View.GONE or View.VISIBLE) + @JvmField var visibTitle: Int + @JvmField var title: String? = null + @JvmField var selfAttendeeStatus = 0 + @JvmField var id: Long = 0 + @JvmField var start: Long = 0 + @JvmField var end: Long = 0 + @JvmField var allDay = false + @JvmField var color = 0 + + @Override + override fun toString(): String { + val builder = StringBuilder() + builder.append("EventInfo [visibTitle=") + builder.append(visibTitle) + builder.append(", title=") + builder.append(title) + builder.append(", visibWhen=") + builder.append(visibWhen) + builder.append(", id=") + builder.append(id) + builder.append(", when=") + builder.append(`when`) + builder.append(", visibWhere=") + builder.append(visibWhere) + builder.append(", where=") + builder.append(where) + builder.append(", color=") + builder.append(String.format("0x%x", color)) + builder.append(", selfAttendeeStatus=") + builder.append(selfAttendeeStatus) + builder.append("]") + return builder.toString() + } + + @Override + override fun hashCode(): Int { + val prime = 31 + var result = 1 + result = prime * result + if (allDay) 1231 else 1237 + result = prime * result + (id xor (id ushr 32)).toInt() + result = prime * result + (end xor (end ushr 32)).toInt() + result = prime * result + (start xor (start ushr 32)).toInt() + result = prime * result + if (title == null) 0 else title!!.hashCode() + result = prime * result + visibTitle + result = prime * result + visibWhen + result = prime * result + visibWhere + result = prime * result + if (`when` == null) 0 else `when`!!.hashCode() + result = prime * result + if (where == null) 0 else where!!.hashCode() + result = prime * result + color + result = prime * result + selfAttendeeStatus + return result + } + + @Override + override fun equals(obj: Any?): Boolean { + if (this == obj) return true + if (obj == null) return false + if (this::class != obj::class) return false + val other = obj as EventInfo + if (id != other.id) return false + if (allDay != other.allDay) return false + if (end != other.end) return false + if (start != other.start) return false + if (title == null) { + if (other.title != null) return false + } else if (!title!!.equals(other.title)) return false + if (visibTitle != other.visibTitle) return false + if (visibWhen != other.visibWhen) return false + if (visibWhere != other.visibWhere) return false + if (`when` == null) { + if (other.`when` != null) return false + } else if (!`when`!!.equals(other.`when`)) { + return false + } + if (where == null) { + if (other.where != null) return false + } else if (!where!!.equals(other.where)) { + return false + } + if (color != other.color) { + return false + } + return if (selfAttendeeStatus != other.selfAttendeeStatus) { + false + } else true + } + + init { + visibWhen = View.GONE + visibWhere = View.GONE + visibTitle = View.GONE + } + } + + /** + * [DayInfo] is a class that represents a day header in the widget. It + * contains all of the data necessary to display that day header, including + * the properly localized string. + */ + internal class DayInfo( + /** The Julian day */ + @JvmField var mJulianDay: Int, + /** The string representation of this day header, to be displayed */ + @JvmField var mDayLabel: String? = null + ) { + @Override + override fun toString(): String { + return mDayLabel as String + } + + @Override + override fun hashCode(): Int { + val prime = 31 + var result = 1 + result = prime * result + (mDayLabel?.hashCode() ?: 0) + result = prime * result + mJulianDay + return result + } + + @Override + override fun equals(obj: Any?): Boolean { + if (this == obj) return true + if (obj == null) return false + if (this::class !== obj::class) return false + val other = obj as DayInfo + if (mDayLabel == null) { + if (other.mDayLabel != null) return false + } else if (!mDayLabel.equals(other.mDayLabel)) return false + return if (mJulianDay != other.mJulianDay) false else true + } + } + + @JvmField val mRowInfos: ArrayList<RowInfo> + @JvmField val mEventInfos: ArrayList<EventInfo> + @JvmField val mDayInfos: ArrayList<DayInfo> + @JvmField val mContext: Context? + @JvmField val mNow: Long + @JvmField val mTodayJulianDay: Int + @JvmField val mMaxJulianDay: Int + fun buildFromCursor(cursor: Cursor, timeZone: String?) { + val recycle = Time(timeZone) + val mBuckets: ArrayList<LinkedList<RowInfo>> = + ArrayList<LinkedList<RowInfo>>(CalendarAppWidgetService.MAX_DAYS) + for (i in 0 until CalendarAppWidgetService.MAX_DAYS) { + mBuckets.add(LinkedList<RowInfo>()) + } + recycle.setToNow() + mShowTZ = !TextUtils.equals(timeZone, Time.getCurrentTimezone()) + if (mShowTZ) { + mHomeTZName = TimeZone.getTimeZone(timeZone).getDisplayName( + recycle.isDst !== 0, + TimeZone.SHORT + ) + } + cursor.moveToPosition(-1) + val tz = Utils.getTimeZone(mContext, null) + while (cursor.moveToNext()) { + val rowId: Int = cursor.getPosition() + val eventId: Long = cursor.getLong(CalendarAppWidgetService.INDEX_EVENT_ID) + val allDay = cursor.getInt(CalendarAppWidgetService.INDEX_ALL_DAY) !== 0 + var start: Long = cursor.getLong(CalendarAppWidgetService.INDEX_BEGIN) + var end: Long = cursor.getLong(CalendarAppWidgetService.INDEX_END) + val title: String = cursor.getString(CalendarAppWidgetService.INDEX_TITLE) + val location: String = cursor.getString(CalendarAppWidgetService.INDEX_EVENT_LOCATION) + // we don't compute these ourselves because it seems to produce the + // wrong endDay for all day events + val startDay: Int = cursor.getInt(CalendarAppWidgetService.INDEX_START_DAY) + val endDay: Int = cursor.getInt(CalendarAppWidgetService.INDEX_END_DAY) + val color: Int = cursor.getInt(CalendarAppWidgetService.INDEX_COLOR) + val selfStatus: Int = cursor + .getInt(CalendarAppWidgetService.INDEX_SELF_ATTENDEE_STATUS) + + // Adjust all-day times into local timezone + if (allDay) { + start = Utils.convertAlldayUtcToLocal(recycle, start, tz as String) + end = Utils.convertAlldayUtcToLocal(recycle, end, tz as String) + } + if (LOGD) { + Log.d( + TAG, "Row #" + rowId + " allDay:" + allDay + " start:" + start + + " end:" + end + " eventId:" + eventId + ) + } + + // we might get some extra events when querying, in order to + // deal with all-day events + if (end < mNow) { + continue + } + val i: Int = mEventInfos.size + mEventInfos.add( + populateEventInfo( + eventId, allDay, start, end, startDay, endDay, title, + location, color, selfStatus + ) + ) + // populate the day buckets that this event falls into + val from: Int = Math.max(startDay, mTodayJulianDay) + val to: Int = Math.min(endDay, mMaxJulianDay) + for (day in from..to) { + val bucket: LinkedList<RowInfo> = mBuckets.get(day - mTodayJulianDay) + val rowInfo = RowInfo(RowInfo.TYPE_MEETING, i) + if (allDay) { + bucket.addFirst(rowInfo) + } else { + bucket.add(rowInfo) + } + } + } + var day = mTodayJulianDay + var count = 0 + for (bucket in mBuckets) { + if (!bucket.isEmpty()) { + // We don't show day header in today + if (day != mTodayJulianDay) { + val dayInfo = populateDayInfo(day, recycle) + // Add the day header + val dayIndex: Int = mDayInfos.size + mDayInfos.add(dayInfo as CalendarAppWidgetModel.DayInfo) + mRowInfos.add(RowInfo(RowInfo.TYPE_DAY, dayIndex)) + } + + // Add the event row infos + mRowInfos.addAll(bucket) + count += bucket.size + } + day++ + if (count >= CalendarAppWidgetService.EVENT_MIN_COUNT) { + break + } + } + } + + private fun populateEventInfo( + eventId: Long, + allDay: Boolean, + start: Long, + end: Long, + startDay: Int, + endDay: Int, + title: String, + location: String, + color: Int, + selfStatus: Int + ): EventInfo { + val eventInfo = EventInfo() + + // Compute a human-readable string for the start time of the event + val whenString = StringBuilder() + val visibWhen: Int + var flags: Int = DateUtils.FORMAT_ABBREV_ALL + visibWhen = View.VISIBLE + if (allDay) { + flags = flags or DateUtils.FORMAT_SHOW_DATE + whenString.append(Utils.formatDateRange(mContext, start, end, flags)) + } else { + flags = flags or DateUtils.FORMAT_SHOW_TIME + if (DateFormat.is24HourFormat(mContext)) { + flags = flags or DateUtils.FORMAT_24HOUR + } + if (endDay > startDay) { + flags = flags or DateUtils.FORMAT_SHOW_DATE + } + whenString.append(Utils.formatDateRange(mContext, start, end, flags)) + if (mShowTZ) { + whenString.append(" ").append(mHomeTZName) + } + } + eventInfo.id = eventId + eventInfo.start = start + eventInfo.end = end + eventInfo.allDay = allDay + eventInfo.`when` = whenString.toString() + eventInfo.visibWhen = visibWhen + eventInfo.color = color + eventInfo.selfAttendeeStatus = selfStatus + + // What + if (TextUtils.isEmpty(title)) { + eventInfo.title = mContext?.getString(R.string.no_title_label) + } else { + eventInfo.title = title + } + eventInfo.visibTitle = View.VISIBLE + + // Where + if (!TextUtils.isEmpty(location)) { + eventInfo.visibWhere = View.VISIBLE + eventInfo.where = location + } else { + eventInfo.visibWhere = View.GONE + } + return eventInfo + } + + private fun populateDayInfo(julianDay: Int, recycle: Time?): DayInfo? { + val millis: Long = recycle?.setJulianDay(julianDay) as Long + var flags: Int = DateUtils.FORMAT_ABBREV_ALL or DateUtils.FORMAT_SHOW_DATE + val label: String? + if (julianDay == mTodayJulianDay + 1) { + label = mContext?.getString( + R.string.agenda_tomorrow, + Utils.formatDateRange(mContext, millis, millis, flags).toString() + ) + } else { + flags = flags or DateUtils.FORMAT_SHOW_WEEKDAY + label = Utils.formatDateRange(mContext, millis, millis, flags) + } + return DayInfo(julianDay, label as String) + } + + @Override + override fun toString(): String { + val builder = StringBuilder() + builder.append("\nCalendarAppWidgetModel [eventInfos=") + builder.append(mEventInfos) + builder.append("]") + return builder.toString() + } + + companion object { + private val TAG: String = CalendarAppWidgetModel::class.java.getSimpleName() + private const val LOGD = false + } + + init { + mNow = System.currentTimeMillis() + val time = Time(timeZone) + time.setToNow() // This is needed for gmtoff to be set + mTodayJulianDay = Time.getJulianDay(mNow, time.gmtoff) + mMaxJulianDay = mTodayJulianDay + CalendarAppWidgetService.MAX_DAYS - 1 + mEventInfos = ArrayList<EventInfo>(50) + mRowInfos = ArrayList<RowInfo>(50) + mDayInfos = ArrayList<DayInfo>(8) + mContext = context + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/widget/CalendarAppWidgetProvider.java b/src/com/android/calendar/widget/CalendarAppWidgetProvider.java deleted file mode 100644 index 3a69efd3..00000000 --- a/src/com/android/calendar/widget/CalendarAppWidgetProvider.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2009 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.calendar.widget; - -import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY; -import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; -import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.provider.CalendarContract; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; -import android.widget.RemoteViews; - -import com.android.calendar.AllInOneActivity; -import com.android.calendar.EventInfoActivity; -import com.android.calendar.R; -import com.android.calendar.Utils; - -/** - * Simple widget to show next upcoming calendar event. - */ -public class CalendarAppWidgetProvider extends AppWidgetProvider { - static final String TAG = "CalendarAppWidgetProvider"; - static final boolean LOGD = false; - - // TODO Move these to Calendar.java - static final String EXTRA_EVENT_IDS = "com.android.calendar.EXTRA_EVENT_IDS"; - - /** - * {@inheritDoc} - */ - @Override - public void onReceive(Context context, Intent intent) { - // Handle calendar-specific updates ourselves because they might be - // coming in without extras, which AppWidgetProvider then blocks. - final String action = intent.getAction(); - if (LOGD) - Log.d(TAG, "AppWidgetProvider got the intent: " + intent.toString()); - if (Utils.getWidgetUpdateAction(context).equals(action)) { - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); - performUpdate(context, appWidgetManager, - appWidgetManager.getAppWidgetIds(getComponentName(context)), - null /* no eventIds */); - } else if (action != null && (action.equals(Intent.ACTION_PROVIDER_CHANGED) - || action.equals(Intent.ACTION_TIME_CHANGED) - || action.equals(Intent.ACTION_TIMEZONE_CHANGED) - || action.equals(Intent.ACTION_DATE_CHANGED) - || action.equals(Utils.getWidgetScheduledUpdateAction(context)))) { - Intent service = new Intent(context, CalendarAppWidgetService.class); - context.startService(service); - } else { - super.onReceive(context, intent); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void onDisabled(Context context) { - // Unsubscribe from all AlarmManager updates - AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - PendingIntent pendingUpdate = getUpdateIntent(context); - am.cancel(pendingUpdate); - } - - /** - * {@inheritDoc} - */ - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - performUpdate(context, appWidgetManager, appWidgetIds, null /* no eventIds */); - } - - - /** - * Build {@link ComponentName} describing this specific - * {@link AppWidgetProvider} - */ - static ComponentName getComponentName(Context context) { - return new ComponentName(context, CalendarAppWidgetProvider.class); - } - - /** - * Process and push out an update for the given appWidgetIds. This call - * actually fires an intent to start {@link CalendarAppWidgetService} as a - * background service which handles the actual update, to prevent ANR'ing - * during database queries. - * - * @param context Context to use when starting {@link CalendarAppWidgetService}. - * @param appWidgetIds List of specific appWidgetIds to update, or null for - * all. - * @param changedEventIds Specific events known to be changed. If present, - * we use it to decide if an update is necessary. - */ - private void performUpdate(Context context, - AppWidgetManager appWidgetManager, int[] appWidgetIds, - long[] changedEventIds) { - // Launch over to service so it can perform update - for (int appWidgetId : appWidgetIds) { - if (LOGD) Log.d(TAG, "Building widget update..."); - Intent updateIntent = new Intent(context, CalendarAppWidgetService.class); - updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - if (changedEventIds != null) { - updateIntent.putExtra(EXTRA_EVENT_IDS, changedEventIds); - } - updateIntent.setData(Uri.parse(updateIntent.toUri(Intent.URI_INTENT_SCHEME))); - - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget); - // Calendar header - Time time = new Time(Utils.getTimeZone(context, null)); - time.setToNow(); - long millis = time.toMillis(true); - final String dayOfWeek = DateUtils.getDayOfWeekString(time.weekDay + 1, - DateUtils.LENGTH_MEDIUM); - final String date = Utils.formatDateRange(context, millis, millis, - DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_NO_YEAR); - views.setTextViewText(R.id.day_of_week, dayOfWeek); - views.setTextViewText(R.id.date, date); - // Attach to list of events - views.setRemoteAdapter(appWidgetId, R.id.events_list, updateIntent); - appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.events_list); - - - // Launch calendar app when the user taps on the header - final Intent launchCalendarIntent = new Intent(Intent.ACTION_VIEW); - launchCalendarIntent.setClass(context, AllInOneActivity.class); - launchCalendarIntent - .setData(Uri.parse("content://com.android.calendar/time/" + millis)); - final PendingIntent launchCalendarPendingIntent = PendingIntent.getActivity( - context, 0 /* no requestCode */, launchCalendarIntent, 0 /* no flags */); - views.setOnClickPendingIntent(R.id.header, launchCalendarPendingIntent); - - // Each list item will call setOnClickExtra() to let the list know - // which item - // is selected by a user. - final PendingIntent updateEventIntent = getLaunchPendingIntentTemplate(context); - views.setPendingIntentTemplate(R.id.events_list, updateEventIntent); - - appWidgetManager.updateAppWidget(appWidgetId, views); - } - } - - /** - * Build the {@link PendingIntent} used to trigger an update of all calendar - * widgets. Uses {@link Utils#getWidgetScheduledUpdateAction(Context)} to - * directly target all widgets instead of using - * {@link AppWidgetManager#EXTRA_APPWIDGET_IDS}. - * - * @param context Context to use when building broadcast. - */ - static PendingIntent getUpdateIntent(Context context) { - Intent intent = new Intent(Utils.getWidgetScheduledUpdateAction(context)); - intent.setDataAndType(CalendarContract.CONTENT_URI, Utils.APPWIDGET_DATA_TYPE); - return PendingIntent.getBroadcast(context, 0 /* no requestCode */, intent, - 0 /* no flags */); - } - - /** - * Build a {@link PendingIntent} to launch the Calendar app. This should be used - * in combination with {@link RemoteViews#setPendingIntentTemplate(int, PendingIntent)}. - */ - static PendingIntent getLaunchPendingIntentTemplate(Context context) { - Intent launchIntent = new Intent(); - launchIntent.setAction(Intent.ACTION_VIEW); - launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | - Intent.FLAG_ACTIVITY_TASK_ON_HOME); - launchIntent.setClass(context, AllInOneActivity.class); - return PendingIntent.getActivity(context, 0 /* no requestCode */, launchIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - - /** - * Build an {@link Intent} available as FillInIntent to launch the Calendar app. - * This should be used in combination with - * {@link RemoteViews#setOnClickFillInIntent(int, Intent)}. - * If the go to time is 0, then calendar will be launched without a starting time. - * - * @param goToTime time that calendar should take the user to, or 0 to - * indicate no specific start time. - */ - static Intent getLaunchFillInIntent(Context context, long id, long start, long end, - boolean allDay) { - final Intent fillInIntent = new Intent(); - String dataString = "content://com.android.calendar/events"; - if (id != 0) { - fillInIntent.putExtra(Utils.INTENT_KEY_DETAIL_VIEW, true); - fillInIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | - Intent.FLAG_ACTIVITY_TASK_ON_HOME); - - dataString += "/" + id; - // If we have an event id - start the event info activity - fillInIntent.setClass(context, EventInfoActivity.class); - } else { - // If we do not have an event id - start AllInOne - fillInIntent.setClass(context, AllInOneActivity.class); - } - Uri data = Uri.parse(dataString); - fillInIntent.setData(data); - fillInIntent.putExtra(EXTRA_EVENT_BEGIN_TIME, start); - fillInIntent.putExtra(EXTRA_EVENT_END_TIME, end); - fillInIntent.putExtra(EXTRA_EVENT_ALL_DAY, allDay); - - return fillInIntent; - } -} diff --git a/src/com/android/calendar/widget/CalendarAppWidgetProvider.kt b/src/com/android/calendar/widget/CalendarAppWidgetProvider.kt new file mode 100644 index 00000000..b3539f22 --- /dev/null +++ b/src/com/android/calendar/widget/CalendarAppWidgetProvider.kt @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2021 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.calendar.widget + +import android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY +import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME +import android.provider.CalendarContract.EXTRA_EVENT_END_TIME +import android.app.AlarmManager +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.CalendarContract +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log +import android.widget.RemoteViews +import com.android.calendar.AllInOneActivity +import com.android.calendar.EventInfoActivity +import com.android.calendar.R +import com.android.calendar.Utils + +/** + * Simple widget to show next upcoming calendar event. + */ +class CalendarAppWidgetProvider : AppWidgetProvider() { + /** + * {@inheritDoc} + */ + @Override + override fun onReceive(context: Context?, intent: Intent?) { + // Handle calendar-specific updates ourselves because they might be + // coming in without extras, which AppWidgetProvider then blocks. + val action: String? = intent?.getAction() + if (LOGD) Log.d(TAG, "AppWidgetProvider got the intent: " + intent.toString()) + if (Utils.getWidgetUpdateAction(context as Context).equals(action)) { + val appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) + performUpdate( + context as Context, appWidgetManager, + appWidgetManager.getAppWidgetIds(getComponentName(context)), + null /* no eventIds */ + ) + } else if (action != null && (action.equals(Intent.ACTION_PROVIDER_CHANGED) || + action.equals(Intent.ACTION_TIME_CHANGED) || + action.equals(Intent.ACTION_TIMEZONE_CHANGED) || + action.equals(Intent.ACTION_DATE_CHANGED) || + action.equals(Utils.getWidgetScheduledUpdateAction(context as Context))) + ) { + val service = Intent(context, CalendarAppWidgetService::class.java) + context?.startService(service) + } else { + super.onReceive(context, intent) + } + } + + /** + * {@inheritDoc} + */ + @Override + override fun onDisabled(context: Context) { + // Unsubscribe from all AlarmManager updates + val am: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val pendingUpdate: PendingIntent = getUpdateIntent(context) + am.cancel(pendingUpdate) + } + + /** + * {@inheritDoc} + */ + @Override + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + performUpdate(context, appWidgetManager, + appWidgetIds, null /* no eventIds */) + } + + /** + * Process and push out an update for the given appWidgetIds. This call + * actually fires an intent to start [CalendarAppWidgetService] as a + * background service which handles the actual update, to prevent ANR'ing + * during database queries. + * + * @param context Context to use when starting [CalendarAppWidgetService]. + * @param appWidgetIds List of specific appWidgetIds to update, or null for + * all. + * @param changedEventIds Specific events known to be changed. If present, + * we use it to decide if an update is necessary. + */ + private fun performUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + changedEventIds: LongArray? + ) { + // Launch over to service so it can perform update + for (appWidgetId in appWidgetIds) { + if (LOGD) Log.d(TAG, "Building widget update...") + val updateIntent = Intent(context, CalendarAppWidgetService::class.java) + updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + if (changedEventIds != null) { + updateIntent.putExtra(EXTRA_EVENT_IDS, changedEventIds) + } + updateIntent.setData(Uri.parse(updateIntent.toUri(Intent.URI_INTENT_SCHEME))) + val views = RemoteViews(context.getPackageName(), R.layout.appwidget) + // Calendar header + val time = Time(Utils.getTimeZone(context, null)) + time.setToNow() + val millis: Long = time.toMillis(true) + val dayOfWeek: String = DateUtils.getDayOfWeekString( + time.weekDay + 1, + DateUtils.LENGTH_MEDIUM + ) + val date: String? = Utils.formatDateRange( + context, millis, millis, + DateUtils.FORMAT_ABBREV_ALL or DateUtils.FORMAT_SHOW_DATE + or DateUtils.FORMAT_NO_YEAR + ) + views.setTextViewText(R.id.day_of_week, dayOfWeek) + views.setTextViewText(R.id.date, date) + // Attach to list of events + views.setRemoteAdapter(appWidgetId, R.id.events_list, updateIntent) + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.events_list) + + // Launch calendar app when the user taps on the header + val launchCalendarIntent = Intent(Intent.ACTION_VIEW) + launchCalendarIntent.setClass(context, AllInOneActivity::class.java) + launchCalendarIntent + .setData(Uri.parse("content://com.android.calendar/time/$millis")) + val launchCalendarPendingIntent: PendingIntent = PendingIntent.getActivity( + context, 0 /* no requestCode */, launchCalendarIntent, 0 /* no flags */ + ) + views.setOnClickPendingIntent(R.id.header, launchCalendarPendingIntent) + + // Each list item will call setOnClickExtra() to let the list know + // which item + // is selected by a user. + val updateEventIntent: PendingIntent = getLaunchPendingIntentTemplate(context) + views.setPendingIntentTemplate(R.id.events_list, updateEventIntent) + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } + + companion object { + const val TAG = "CalendarAppWidgetProvider" + const val LOGD = false + + // TODO Move these to Calendar.java + const val EXTRA_EVENT_IDS = "com.android.calendar.EXTRA_EVENT_IDS" + + /** + * Build [ComponentName] describing this specific + * [AppWidgetProvider] + */ + @JvmStatic fun getComponentName(context: Context?): ComponentName { + return ComponentName(context as Context, CalendarAppWidgetProvider::class.java) + } + + /** + * Build the [PendingIntent] used to trigger an update of all calendar + * widgets. Uses [Utils.getWidgetScheduledUpdateAction] to + * directly target all widgets instead of using + * [AppWidgetManager.EXTRA_APPWIDGET_IDS]. + * + * @param context Context to use when building broadcast. + */ + @JvmStatic fun getUpdateIntent(context: Context?): PendingIntent { + val intent = Intent(Utils.getWidgetScheduledUpdateAction(context as Context)) + intent.setDataAndType(CalendarContract.CONTENT_URI, Utils.APPWIDGET_DATA_TYPE) + return PendingIntent.getBroadcast( + context, 0 /* no requestCode */, intent, + 0 /* no flags */ + ) + } + + /** + * Build a [PendingIntent] to launch the Calendar app. This should be used + * in combination with [RemoteViews.setPendingIntentTemplate]. + */ + @JvmStatic fun getLaunchPendingIntentTemplate(context: Context?): PendingIntent { + val launchIntent = Intent() + launchIntent.setAction(Intent.ACTION_VIEW) + launchIntent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_ACTIVITY_TASK_ON_HOME + ) + launchIntent.setClass(context as Context, AllInOneActivity::class.java) + return PendingIntent.getActivity( + context, 0 /* no requestCode */, launchIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + } + + /** + * Build an [Intent] available as FillInIntent to launch the Calendar app. + * This should be used in combination with + * [RemoteViews.setOnClickFillInIntent]. + * If the go to time is 0, then calendar will be launched without a starting time. + * + * @param goToTime time that calendar should take the user to, or 0 to + * indicate no specific start time. + */ + @JvmStatic fun getLaunchFillInIntent( + context: Context?, + id: Long, + start: Long, + end: Long, + allDay: Boolean + ): Intent { + val fillInIntent = Intent() + var dataString = "content://com.android.calendar/events" + if (id != 0L) { + fillInIntent.putExtra(Utils.INTENT_KEY_DETAIL_VIEW, true) + fillInIntent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_ACTIVITY_TASK_ON_HOME + ) + dataString += "/$id" + // If we have an event id - start the event info activity + fillInIntent.setClass(context as Context, EventInfoActivity::class.java) + } else { + // If we do not have an event id - start AllInOne + fillInIntent.setClass(context as Context, AllInOneActivity::class.java) + } + val data: Uri = Uri.parse(dataString) + fillInIntent.setData(data) + fillInIntent.putExtra(EXTRA_EVENT_BEGIN_TIME, start) + fillInIntent.putExtra(EXTRA_EVENT_END_TIME, end) + fillInIntent.putExtra(EXTRA_EVENT_ALL_DAY, allDay) + return fillInIntent + } + } +}
\ No newline at end of file diff --git a/src/com/android/calendar/widget/CalendarAppWidgetService.java b/src/com/android/calendar/widget/CalendarAppWidgetService.java deleted file mode 100644 index ec702c7c..00000000 --- a/src/com/android/calendar/widget/CalendarAppWidgetService.java +++ /dev/null @@ -1,626 +0,0 @@ -/* - * Copyright (C) 2009 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.calendar.widget; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.CursorLoader; -import android.content.Intent; -import android.content.Loader; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.os.Handler; -import android.provider.CalendarContract.Attendees; -import android.provider.CalendarContract.Calendars; -import android.provider.CalendarContract.Instances; -import android.text.format.DateUtils; -import android.text.format.Time; -import android.util.Log; -import android.view.View; -import android.widget.RemoteViews; -import android.widget.RemoteViewsService; - -import com.android.calendar.R; -import com.android.calendar.Utils; -import com.android.calendar.widget.CalendarAppWidgetModel.DayInfo; -import com.android.calendar.widget.CalendarAppWidgetModel.EventInfo; -import com.android.calendar.widget.CalendarAppWidgetModel.RowInfo; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; - - -public class CalendarAppWidgetService extends RemoteViewsService { - private static final String TAG = "CalendarWidget"; - - static final int EVENT_MIN_COUNT = 20; - static final int EVENT_MAX_COUNT = 100; - // Minimum delay between queries on the database for widget updates in ms - static final int WIDGET_UPDATE_THROTTLE = 500; - - private static final String EVENT_SORT_ORDER = Instances.START_DAY + " ASC, " - + Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, " - + Instances.END_MINUTE + " ASC LIMIT " + EVENT_MAX_COUNT; - - private static final String EVENT_SELECTION = Calendars.VISIBLE + "=1"; - private static final String EVENT_SELECTION_HIDE_DECLINED = Calendars.VISIBLE + "=1 AND " - + Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED; - - static final String[] EVENT_PROJECTION = new String[] { - Instances.ALL_DAY, - Instances.BEGIN, - Instances.END, - Instances.TITLE, - Instances.EVENT_LOCATION, - Instances.EVENT_ID, - Instances.START_DAY, - Instances.END_DAY, - Instances.DISPLAY_COLOR, // If SDK < 16, set to Instances.CALENDAR_COLOR. - Instances.SELF_ATTENDEE_STATUS, - }; - - static final int INDEX_ALL_DAY = 0; - static final int INDEX_BEGIN = 1; - static final int INDEX_END = 2; - static final int INDEX_TITLE = 3; - static final int INDEX_EVENT_LOCATION = 4; - static final int INDEX_EVENT_ID = 5; - static final int INDEX_START_DAY = 6; - static final int INDEX_END_DAY = 7; - static final int INDEX_COLOR = 8; - static final int INDEX_SELF_ATTENDEE_STATUS = 9; - - static { - if (!Utils.isJellybeanOrLater()) { - EVENT_PROJECTION[INDEX_COLOR] = Instances.CALENDAR_COLOR; - } - } - static final int MAX_DAYS = 7; - - private static final long SEARCH_DURATION = MAX_DAYS * DateUtils.DAY_IN_MILLIS; - - /** - * Update interval used when no next-update calculated, or bad trigger time in past. - * Unit: milliseconds. - */ - private static final long UPDATE_TIME_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6; - - @Override - public RemoteViewsFactory onGetViewFactory(Intent intent) { - return new CalendarFactory(getApplicationContext(), intent); - } - - public static class CalendarFactory extends BroadcastReceiver implements - RemoteViewsService.RemoteViewsFactory, Loader.OnLoadCompleteListener<Cursor> { - private static final boolean LOGD = false; - - // Suppress unnecessary logging about update time. Need to be static as this object is - // re-instanciated frequently. - // TODO: It seems loadData() is called via onCreate() four times, which should mean - // unnecessary CalendarFactory object is created and dropped. It is not efficient. - private static long sLastUpdateTime = UPDATE_TIME_NO_EVENTS; - - private Context mContext; - private Resources mResources; - private static CalendarAppWidgetModel mModel; - private static Object mLock = new Object(); - private static volatile int mSerialNum = 0; - private int mLastSerialNum = -1; - private CursorLoader mLoader; - private final Handler mHandler = new Handler(); - private static final AtomicInteger currentVersion = new AtomicInteger(0); - private final ExecutorService executor = Executors.newSingleThreadExecutor(); - private int mAppWidgetId; - private int mDeclinedColor; - private int mStandardColor; - private int mAllDayColor; - - private final Runnable mTimezoneChanged = new Runnable() { - @Override - public void run() { - if (mLoader != null) { - mLoader.forceLoad(); - } - } - }; - - private Runnable createUpdateLoaderRunnable(final String selection, - final PendingResult result, final int version) { - return new Runnable() { - @Override - public void run() { - // If there is a newer load request in the queue, skip loading. - if (mLoader != null && version >= currentVersion.get()) { - Uri uri = createLoaderUri(); - mLoader.setUri(uri); - mLoader.setSelection(selection); - synchronized (mLock) { - mLastSerialNum = ++mSerialNum; - } - mLoader.forceLoad(); - } - result.finish(); - } - }; - } - - protected CalendarFactory(Context context, Intent intent) { - mContext = context; - mResources = context.getResources(); - mAppWidgetId = intent.getIntExtra( - AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - - mDeclinedColor = mResources.getColor(R.color.appwidget_item_declined_color); - mStandardColor = mResources.getColor(R.color.appwidget_item_standard_color); - mAllDayColor = mResources.getColor(R.color.appwidget_item_allday_color); - } - - public CalendarFactory() { - // This is being created as part of onReceive - - } - - @Override - public void onCreate() { - String selection = queryForSelection(); - initLoader(selection); - } - - @Override - public void onDataSetChanged() { - } - - @Override - public void onDestroy() { - if (mLoader != null) { - mLoader.reset(); - } - } - - @Override - public RemoteViews getLoadingView() { - RemoteViews views = new RemoteViews(mContext.getPackageName(), - R.layout.appwidget_loading); - return views; - } - - @Override - public RemoteViews getViewAt(int position) { - // we use getCount here so that it doesn't return null when empty - if (position < 0 || position >= getCount()) { - return null; - } - - if (mModel == null) { - RemoteViews views = new RemoteViews(mContext.getPackageName(), - R.layout.appwidget_loading); - final Intent intent = CalendarAppWidgetProvider.getLaunchFillInIntent(mContext, 0, - 0, 0, false); - views.setOnClickFillInIntent(R.id.appwidget_loading, intent); - return views; - - } - if (mModel.mEventInfos.isEmpty() || mModel.mRowInfos.isEmpty()) { - RemoteViews views = new RemoteViews(mContext.getPackageName(), - R.layout.appwidget_no_events); - final Intent intent = CalendarAppWidgetProvider.getLaunchFillInIntent(mContext, 0, - 0, 0, false); - views.setOnClickFillInIntent(R.id.appwidget_no_events, intent); - return views; - } - - RowInfo rowInfo = mModel.mRowInfos.get(position); - if (rowInfo.mType == RowInfo.TYPE_DAY) { - RemoteViews views = new RemoteViews(mContext.getPackageName(), - R.layout.appwidget_day); - DayInfo dayInfo = mModel.mDayInfos.get(rowInfo.mIndex); - updateTextView(views, R.id.date, View.VISIBLE, dayInfo.mDayLabel); - return views; - } else { - RemoteViews views; - final EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex); - if (eventInfo.allDay) { - views = new RemoteViews(mContext.getPackageName(), - R.layout.widget_all_day_item); - } else { - views = new RemoteViews(mContext.getPackageName(), R.layout.widget_item); - } - int displayColor = Utils.getDisplayColorFromColor(eventInfo.color); - - final long now = System.currentTimeMillis(); - if (!eventInfo.allDay && eventInfo.start <= now && now <= eventInfo.end) { - views.setInt(R.id.widget_row, "setBackgroundResource", - R.drawable.agenda_item_bg_secondary); - } else { - views.setInt(R.id.widget_row, "setBackgroundResource", - R.drawable.agenda_item_bg_primary); - } - - if (!eventInfo.allDay) { - updateTextView(views, R.id.when, eventInfo.visibWhen, eventInfo.when); - updateTextView(views, R.id.where, eventInfo.visibWhere, eventInfo.where); - } - updateTextView(views, R.id.title, eventInfo.visibTitle, eventInfo.title); - - views.setViewVisibility(R.id.agenda_item_color, View.VISIBLE); - - int selfAttendeeStatus = eventInfo.selfAttendeeStatus; - if (eventInfo.allDay) { - if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_INVITED) { - views.setInt(R.id.agenda_item_color, "setImageResource", - R.drawable.widget_chip_not_responded_bg); - views.setInt(R.id.title, "setTextColor", displayColor); - } else { - views.setInt(R.id.agenda_item_color, "setImageResource", - R.drawable.widget_chip_responded_bg); - views.setInt(R.id.title, "setTextColor", mAllDayColor); - } - if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED) { - // 40% opacity - views.setInt(R.id.agenda_item_color, "setColorFilter", - Utils.getDeclinedColorFromColor(displayColor)); - } else { - views.setInt(R.id.agenda_item_color, "setColorFilter", displayColor); - } - } else if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED) { - views.setInt(R.id.title, "setTextColor", mDeclinedColor); - views.setInt(R.id.when, "setTextColor", mDeclinedColor); - views.setInt(R.id.where, "setTextColor", mDeclinedColor); - views.setInt(R.id.agenda_item_color, "setImageResource", - R.drawable.widget_chip_responded_bg); - // 40% opacity - views.setInt(R.id.agenda_item_color, "setColorFilter", - Utils.getDeclinedColorFromColor(displayColor)); - } else { - views.setInt(R.id.title, "setTextColor", mStandardColor); - views.setInt(R.id.when, "setTextColor", mStandardColor); - views.setInt(R.id.where, "setTextColor", mStandardColor); - if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_INVITED) { - views.setInt(R.id.agenda_item_color, "setImageResource", - R.drawable.widget_chip_not_responded_bg); - } else { - views.setInt(R.id.agenda_item_color, "setImageResource", - R.drawable.widget_chip_responded_bg); - } - views.setInt(R.id.agenda_item_color, "setColorFilter", displayColor); - } - - long start = eventInfo.start; - long end = eventInfo.end; - // An element in ListView. - if (eventInfo.allDay) { - String tz = Utils.getTimeZone(mContext, null); - Time recycle = new Time(); - start = Utils.convertAlldayLocalToUTC(recycle, start, tz); - end = Utils.convertAlldayLocalToUTC(recycle, end, tz); - } - final Intent fillInIntent = CalendarAppWidgetProvider.getLaunchFillInIntent( - mContext, eventInfo.id, start, end, eventInfo.allDay); - views.setOnClickFillInIntent(R.id.widget_row, fillInIntent); - return views; - } - } - - @Override - public int getViewTypeCount() { - return 5; - } - - @Override - public int getCount() { - // if there are no events, we still return 1 to represent the "no - // events" view - if (mModel == null) { - return 1; - } - return Math.max(1, mModel.mRowInfos.size()); - } - - @Override - public long getItemId(int position) { - if (mModel == null || mModel.mRowInfos.isEmpty() || position >= getCount()) { - return 0; - } - RowInfo rowInfo = mModel.mRowInfos.get(position); - if (rowInfo.mType == RowInfo.TYPE_DAY) { - return rowInfo.mIndex; - } - EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex); - long prime = 31; - long result = 1; - result = prime * result + (int) (eventInfo.id ^ (eventInfo.id >>> 32)); - result = prime * result + (int) (eventInfo.start ^ (eventInfo.start >>> 32)); - return result; - } - - @Override - public boolean hasStableIds() { - return true; - } - - /** - * Query across all calendars for upcoming event instances from now - * until some time in the future. Widen the time range that we query by - * one day on each end so that we can catch all-day events. All-day - * events are stored starting at midnight in UTC but should be included - * in the list of events starting at midnight local time. This may fetch - * more events than we actually want, so we filter them out later. - * - * @param selection The selection string for the loader to filter the query with. - */ - public void initLoader(String selection) { - if (LOGD) - Log.d(TAG, "Querying for widget events..."); - - // Search for events from now until some time in the future - Uri uri = createLoaderUri(); - mLoader = new CursorLoader(mContext, uri, EVENT_PROJECTION, selection, null, - EVENT_SORT_ORDER); - mLoader.setUpdateThrottle(WIDGET_UPDATE_THROTTLE); - synchronized (mLock) { - mLastSerialNum = ++mSerialNum; - } - mLoader.registerListener(mAppWidgetId, this); - mLoader.startLoading(); - - } - - /** - * This gets the selection string for the loader. This ends up doing a query in the - * shared preferences. - */ - private String queryForSelection() { - return Utils.getHideDeclinedEvents(mContext) ? EVENT_SELECTION_HIDE_DECLINED - : EVENT_SELECTION; - } - - /** - * @return The uri for the loader - */ - private Uri createLoaderUri() { - long now = System.currentTimeMillis(); - // Add a day on either side to catch all-day events - long begin = now - DateUtils.DAY_IN_MILLIS; - long end = now + SEARCH_DURATION + DateUtils.DAY_IN_MILLIS; - - Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI, Long.toString(begin) + "/" + end); - return uri; - } - - /* @VisibleForTesting */ - protected static CalendarAppWidgetModel buildAppWidgetModel( - Context context, Cursor cursor, String timeZone) { - CalendarAppWidgetModel model = new CalendarAppWidgetModel(context, timeZone); - model.buildFromCursor(cursor, timeZone); - return model; - } - - /** - * Calculates and returns the next time we should push widget updates. - */ - private long calculateUpdateTime(CalendarAppWidgetModel model, long now, String timeZone) { - // Make sure an update happens at midnight or earlier - long minUpdateTime = getNextMidnightTimeMillis(timeZone); - for (EventInfo event : model.mEventInfos) { - final long start; - final long end; - start = event.start; - end = event.end; - - // We want to update widget when we enter/exit time range of an event. - if (now < start) { - minUpdateTime = Math.min(minUpdateTime, start); - } else if (now < end) { - minUpdateTime = Math.min(minUpdateTime, end); - } - } - return minUpdateTime; - } - - private static long getNextMidnightTimeMillis(String timezone) { - Time time = new Time(); - time.setToNow(); - time.monthDay++; - time.hour = 0; - time.minute = 0; - time.second = 0; - long midnightDeviceTz = time.normalize(true); - - time.timezone = timezone; - time.setToNow(); - time.monthDay++; - time.hour = 0; - time.minute = 0; - time.second = 0; - long midnightHomeTz = time.normalize(true); - - return Math.min(midnightDeviceTz, midnightHomeTz); - } - - static void updateTextView(RemoteViews views, int id, int visibility, String string) { - views.setViewVisibility(id, visibility); - if (visibility == View.VISIBLE) { - views.setTextViewText(id, string); - } - } - - /* - * (non-Javadoc) - * @see - * android.content.Loader.OnLoadCompleteListener#onLoadComplete(android - * .content.Loader, java.lang.Object) - */ - @Override - public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) { - if (cursor == null) { - return; - } - // If a newer update has happened since we started clean up and - // return - synchronized (mLock) { - if (cursor.isClosed()) { - Log.wtf(TAG, "Got a closed cursor from onLoadComplete"); - return; - } - - if (mLastSerialNum != mSerialNum) { - return; - } - - final long now = System.currentTimeMillis(); - String tz = Utils.getTimeZone(mContext, mTimezoneChanged); - - // Copy it to a local static cursor. - MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor); - try { - mModel = buildAppWidgetModel(mContext, matrixCursor, tz); - } finally { - if (matrixCursor != null) { - matrixCursor.close(); - } - - if (cursor != null) { - cursor.close(); - } - } - - // Schedule an alarm to wake ourselves up for the next update. - // We also cancel - // all existing wake-ups because PendingIntents don't match - // against extras. - long triggerTime = calculateUpdateTime(mModel, now, tz); - - // If no next-update calculated, or bad trigger time in past, - // schedule - // update about six hours from now. - if (triggerTime < now) { - Log.w(TAG, "Encountered bad trigger time " + formatDebugTime(triggerTime, now)); - triggerTime = now + UPDATE_TIME_NO_EVENTS; - } - - final AlarmManager alertManager = (AlarmManager) mContext - .getSystemService(Context.ALARM_SERVICE); - final PendingIntent pendingUpdate = CalendarAppWidgetProvider - .getUpdateIntent(mContext); - - alertManager.cancel(pendingUpdate); - alertManager.set(AlarmManager.RTC, triggerTime, pendingUpdate); - Time time = new Time(Utils.getTimeZone(mContext, null)); - time.setToNow(); - - if (time.normalize(true) != sLastUpdateTime) { - Time time2 = new Time(Utils.getTimeZone(mContext, null)); - time2.set(sLastUpdateTime); - time2.normalize(true); - if (time.year != time2.year || time.yearDay != time2.yearDay) { - final Intent updateIntent = new Intent( - Utils.getWidgetUpdateAction(mContext)); - mContext.sendBroadcast(updateIntent); - } - - sLastUpdateTime = time.toMillis(true); - } - - AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext); - if (widgetManager == null) { - return; - } - if (mAppWidgetId == -1) { - int[] ids = widgetManager.getAppWidgetIds(CalendarAppWidgetProvider - .getComponentName(mContext)); - - widgetManager.notifyAppWidgetViewDataChanged(ids, R.id.events_list); - } else { - widgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.events_list); - } - } - } - - @Override - public void onReceive(Context context, Intent intent) { - if (LOGD) - Log.d(TAG, "AppWidgetService received an intent. It was " + intent.toString()); - mContext = context; - - // We cannot do any queries from the UI thread, so push the 'selection' query - // to a background thread. However the implementation of the latter query - // (cursor loading) uses CursorLoader which must be initiated from the UI thread, - // so there is some convoluted handshaking here. - // - // Note that as currently implemented, this must run in a single threaded executor - // or else the loads may be run out of order. - // - // TODO: Remove use of mHandler and CursorLoader, and do all the work synchronously - // in the background thread. All the handshaking going on here between the UI and - // background thread with using goAsync, mHandler, and CursorLoader is confusing. - final PendingResult result = goAsync(); - executor.submit(new Runnable() { - @Override - public void run() { - // We always complete queryForSelection() even if the load task ends up being - // canceled because of a more recent one. Optimizing this to allow - // canceling would require keeping track of all the PendingResults - // (from goAsync) to abort them. Defer this until it becomes a problem. - final String selection = queryForSelection(); - - if (mLoader == null) { - mAppWidgetId = -1; - mHandler.post(new Runnable() { - @Override - public void run() { - initLoader(selection); - result.finish(); - } - }); - } else { - mHandler.post(createUpdateLoaderRunnable(selection, result, - currentVersion.incrementAndGet())); - } - } - }); - } - } - - /** - * Format given time for debugging output. - * - * @param unixTime Target time to report. - * @param now Current system time from {@link System#currentTimeMillis()} - * for calculating time difference. - */ - static String formatDebugTime(long unixTime, long now) { - Time time = new Time(); - time.set(unixTime); - - long delta = unixTime - now; - if (delta > DateUtils.MINUTE_IN_MILLIS) { - delta /= DateUtils.MINUTE_IN_MILLIS; - return String.format("[%d] %s (%+d mins)", unixTime, - time.format("%H:%M:%S"), delta); - } else { - delta /= DateUtils.SECOND_IN_MILLIS; - return String.format("[%d] %s (%+d secs)", unixTime, - time.format("%H:%M:%S"), delta); - } - } -} diff --git a/src/com/android/calendar/widget/CalendarAppWidgetService.kt b/src/com/android/calendar/widget/CalendarAppWidgetService.kt new file mode 100644 index 00000000..114fdf12 --- /dev/null +++ b/src/com/android/calendar/widget/CalendarAppWidgetService.kt @@ -0,0 +1,665 @@ +/* + * Copyright (C) 2021 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.calendar.widget + +import android.app.AlarmManager +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.CursorLoader +import android.content.Intent +import android.content.Loader +import android.content.res.Resources +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.os.Handler +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Instances +import android.text.format.DateUtils +import android.text.format.Time +import android.util.Log +import android.view.View +import android.widget.RemoteViews +import android.widget.RemoteViewsService +import com.android.calendar.R +import com.android.calendar.Utils +import com.android.calendar.widget.CalendarAppWidgetModel.DayInfo +import com.android.calendar.widget.CalendarAppWidgetModel.EventInfo +import com.android.calendar.widget.CalendarAppWidgetModel.RowInfo +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger + +class CalendarAppWidgetService : RemoteViewsService() { + companion object { + private const val TAG = "CalendarWidget" + const val EVENT_MIN_COUNT = 20 + const val EVENT_MAX_COUNT = 100 + + // Minimum delay between queries on the database for widget updates in ms + const val WIDGET_UPDATE_THROTTLE = 500 + private val EVENT_SORT_ORDER: String = (Instances.START_DAY.toString() + " ASC, " + + Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, " + + Instances.END_MINUTE + " ASC LIMIT " + EVENT_MAX_COUNT) + private val EVENT_SELECTION: String = Calendars.VISIBLE.toString() + "=1" + private val EVENT_SELECTION_HIDE_DECLINED: String = + (Calendars.VISIBLE.toString() + "=1 AND " + + Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED) + @JvmField + val EVENT_PROJECTION = arrayOf<String>( + Instances.ALL_DAY, + Instances.BEGIN, + Instances.END, + Instances.TITLE, + Instances.EVENT_LOCATION, + Instances.EVENT_ID, + Instances.START_DAY, + Instances.END_DAY, + Instances.DISPLAY_COLOR, // If SDK < 16, set to Instances.CALENDAR_COLOR. + Instances.SELF_ATTENDEE_STATUS + ) + const val INDEX_ALL_DAY = 0 + const val INDEX_BEGIN = 1 + const val INDEX_END = 2 + const val INDEX_TITLE = 3 + const val INDEX_EVENT_LOCATION = 4 + const val INDEX_EVENT_ID = 5 + const val INDEX_START_DAY = 6 + const val INDEX_END_DAY = 7 + const val INDEX_COLOR = 8 + const val INDEX_SELF_ATTENDEE_STATUS = 9 + const val MAX_DAYS = 7 + private val SEARCH_DURATION: Long = MAX_DAYS * DateUtils.DAY_IN_MILLIS + + /** + * Update interval used when no next-update calculated, or bad trigger time in past. + * Unit: milliseconds. + */ + private val UPDATE_TIME_NO_EVENTS: Long = DateUtils.HOUR_IN_MILLIS * 6 + + /** + * Format given time for debugging output. + * + * @param unixTime Target time to report. + * @param now Current system time from [System.currentTimeMillis] + * for calculating time difference. + */ + fun formatDebugTime(unixTime: Long, now: Long): String { + val time = Time() + time.set(unixTime) + var delta = unixTime - now + return if (delta > DateUtils.MINUTE_IN_MILLIS) { + delta /= DateUtils.MINUTE_IN_MILLIS + String.format( + "[%d] %s (%+d mins)", unixTime, + time.format("%H:%M:%S"), delta + ) + } else { + delta /= DateUtils.SECOND_IN_MILLIS + String.format( + "[%d] %s (%+d secs)", unixTime, + time.format("%H:%M:%S"), delta + ) + } + } + + init { + if (!Utils.isJellybeanOrLater()) { + EVENT_PROJECTION[INDEX_COLOR] = Instances.CALENDAR_COLOR + } + } + } + + @Override + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { + return CalendarFactory(getApplicationContext(), intent) + } + + class CalendarFactory : BroadcastReceiver, RemoteViewsService.RemoteViewsFactory, + Loader.OnLoadCompleteListener<Cursor?> { + private var mContext: Context? = null + private var mResources: Resources? = null + private var mLastSerialNum = -1 + private var mLoader: CursorLoader? = null + private val mHandler: Handler = Handler() + private val executor: ExecutorService = Executors.newSingleThreadExecutor() + private var mAppWidgetId = 0 + private var mDeclinedColor = 0 + private var mStandardColor = 0 + private var mAllDayColor = 0 + private val mTimezoneChanged: Runnable = object : Runnable { + @Override + override fun run() { + if (mLoader != null) { + mLoader?.forceLoad() + } + } + } + + private fun createUpdateLoaderRunnable( + selection: String, + result: PendingResult, + version: Int + ): Runnable { + return object : Runnable { + @Override + override fun run() { + // If there is a newer load request in the queue, skip loading. + if (mLoader != null && version >= currentVersion.get()) { + val uri: Uri = createLoaderUri() + mLoader?.setUri(uri) + mLoader?.setSelection(selection) + synchronized(mLock) { mLastSerialNum = ++mSerialNum } + mLoader?.forceLoad() + } + result.finish() + } + } + } + + constructor(context: Context, intent: Intent) { + mContext = context + mResources = context.getResources() + mAppWidgetId = intent.getIntExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID + ) + mDeclinedColor = mResources?.getColor(R.color.appwidget_item_declined_color) as Int + mStandardColor = mResources?.getColor(R.color.appwidget_item_standard_color) as Int + mAllDayColor = mResources?.getColor(R.color.appwidget_item_allday_color) as Int + } + + constructor() { + // This is being created as part of onReceive + } + + @Override + override fun onCreate() { + val selection = queryForSelection() + initLoader(selection) + } + + @Override + override fun onDataSetChanged() { + } + + @Override + override fun onDestroy() { + if (mLoader != null) { + mLoader?.reset() + } + } + + @Override + override fun getLoadingView(): RemoteViews { + val views = RemoteViews(mContext?.getPackageName(), R.layout.appwidget_loading) + return views + } + + @Override + override fun getViewAt(position: Int): RemoteViews? { + // we use getCount here so that it doesn't return null when empty + if (position < 0 || position >= getCount()) { + return null + } + if (mModel == null) { + val views = RemoteViews( + mContext?.getPackageName(), + R.layout.appwidget_loading + ) + val intent: Intent = CalendarAppWidgetProvider.getLaunchFillInIntent( + mContext, + 0, + 0, + 0, + false + ) + views.setOnClickFillInIntent(R.id.appwidget_loading, intent) + return views + } + if (mModel!!.mEventInfos!!.isEmpty() || mModel!!.mRowInfos!!.isEmpty()) { + val views = RemoteViews( + mContext?.getPackageName(), + R.layout.appwidget_no_events + ) + val intent: Intent = CalendarAppWidgetProvider.getLaunchFillInIntent( + mContext, + 0, + 0, + 0, + false + ) + views.setOnClickFillInIntent(R.id.appwidget_no_events, intent) + return views + } + val rowInfo: RowInfo? = mModel?.mRowInfos?.get(position) + return if (rowInfo!!.mType == RowInfo!!.TYPE_DAY) { + val views = RemoteViews( + mContext?.getPackageName(), + R.layout.appwidget_day + ) + val dayInfo: DayInfo? = mModel?.mDayInfos?.get(rowInfo!!.mIndex) + updateTextView(views, R.id.date, View.VISIBLE, dayInfo!!.mDayLabel) + views + } else { + val views: RemoteViews? + val eventInfo: EventInfo? = mModel?.mEventInfos?.get(rowInfo.mIndex) + if (eventInfo!!.allDay) { + views = RemoteViews( + mContext?.getPackageName(), + R.layout.widget_all_day_item + ) + } else { + views = RemoteViews(mContext?.getPackageName(), R.layout.widget_item) + } + val displayColor: Int = Utils.getDisplayColorFromColor(eventInfo!!.color) + val now: Long = System.currentTimeMillis() + if (!eventInfo!!.allDay && eventInfo!!.start <= now && now <= eventInfo!!.end) { + views?.setInt( + R.id.widget_row, "setBackgroundResource", + R.drawable.agenda_item_bg_secondary + ) + } else { + views?.setInt( + R.id.widget_row, "setBackgroundResource", + R.drawable.agenda_item_bg_primary + ) + } + if (!eventInfo?.allDay) { + updateTextView(views, R.id.`when`, eventInfo?.visibWhen + as Int, eventInfo?.`when`) + updateTextView(views, R.id.where, eventInfo?.visibWhere + as Int, eventInfo?.where) + } + updateTextView(views, R.id.title, eventInfo?.visibTitle as Int, eventInfo?.title) + views.setViewVisibility(R.id.agenda_item_color, View.VISIBLE) + val selfAttendeeStatus: Int = eventInfo?.selfAttendeeStatus as Int + if (eventInfo!!.allDay) { + if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_INVITED) { + views?.setInt( + R.id.agenda_item_color, "setImageResource", + R.drawable.widget_chip_not_responded_bg + ) + views?.setInt(R.id.title, "setTextColor", displayColor) + } else { + views?.setInt( + R.id.agenda_item_color, "setImageResource", + R.drawable.widget_chip_responded_bg + ) + views?.setInt(R.id.title, "setTextColor", mAllDayColor) + } + if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED) { + // 40% opacity + views?.setInt( + R.id.agenda_item_color, "setColorFilter", + Utils.getDeclinedColorFromColor(displayColor) + ) + } else { + views?.setInt(R.id.agenda_item_color, "setColorFilter", displayColor) + } + } else if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED) { + views?.setInt(R.id.title, "setTextColor", mDeclinedColor) + views?.setInt(R.id.`when`, "setTextColor", mDeclinedColor) + views?.setInt(R.id.where, "setTextColor", mDeclinedColor) + views?.setInt( + R.id.agenda_item_color, "setImageResource", + R.drawable.widget_chip_responded_bg + ) + // 40% opacity + views?.setInt( + R.id.agenda_item_color, "setColorFilter", + Utils.getDeclinedColorFromColor(displayColor) + ) + } else { + views?.setInt(R.id.title, "setTextColor", mStandardColor) + views?.setInt(R.id.`when`, "setTextColor", mStandardColor) + views?.setInt(R.id.where, "setTextColor", mStandardColor) + if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_INVITED) { + views?.setInt( + R.id.agenda_item_color, "setImageResource", + R.drawable.widget_chip_not_responded_bg + ) + } else { + views?.setInt( + R.id.agenda_item_color, "setImageResource", + R.drawable.widget_chip_responded_bg + ) + } + views?.setInt(R.id.agenda_item_color, "setColorFilter", displayColor) + } + var start: Long = eventInfo?.start as Long + var end: Long = eventInfo?.end as Long + // An element in ListView. + if (eventInfo!!.allDay) { + val tz: String? = Utils.getTimeZone(mContext, null) + val recycle = Time() + start = Utils.convertAlldayLocalToUTC(recycle, start, tz as String) + end = Utils.convertAlldayLocalToUTC(recycle, end, tz as String) + } + val fillInIntent: Intent = CalendarAppWidgetProvider.getLaunchFillInIntent( + mContext, eventInfo?.id, start, end, eventInfo?.allDay + ) + views.setOnClickFillInIntent(R.id.widget_row, fillInIntent) + views + } + } + + @Override + override fun getViewTypeCount(): Int { + return 5 + } + + @Override + override fun getCount(): Int { + // if there are no events, we still return 1 to represent the "no + // events" view + if (mModel == null) { + return 1 + } + return Math.max(1, mModel?.mRowInfos?.size as Int) + } + + @Override + override fun getItemId(position: Int): Long { + if (mModel == null || mModel?.mRowInfos?.isEmpty() as Boolean || + position >= getCount()) { + return 0 + } + val rowInfo: RowInfo = mModel?.mRowInfos?.get(position) as RowInfo + if (rowInfo.mType == RowInfo.TYPE_DAY) { + return rowInfo.mIndex.toLong() + } + val eventInfo: EventInfo = mModel?.mEventInfos?.get(rowInfo.mIndex) as EventInfo + val prime: Long = 31 + var result: Long = 1 + result = prime * result + (eventInfo.id xor (eventInfo.id ushr 32)) as Int + result = prime * result + (eventInfo.start xor (eventInfo.start ushr 32)) as Int + return result + } + + @Override + override fun hasStableIds(): Boolean { + return true + } + + /** + * Query across all calendars for upcoming event instances from now + * until some time in the future. Widen the time range that we query by + * one day on each end so that we can catch all-day events. All-day + * events are stored starting at midnight in UTC but should be included + * in the list of events starting at midnight local time. This may fetch + * more events than we actually want, so we filter them out later. + * + * @param selection The selection string for the loader to filter the query with. + */ + fun initLoader(selection: String?) { + if (LOGD) Log.d(TAG, "Querying for widget events...") + + // Search for events from now until some time in the future + val uri: Uri = createLoaderUri() + mLoader = CursorLoader( + mContext, uri, EVENT_PROJECTION, selection, null, + EVENT_SORT_ORDER + ) + mLoader?.setUpdateThrottle(WIDGET_UPDATE_THROTTLE.toLong()) + synchronized(mLock) { mLastSerialNum = ++mSerialNum } + mLoader?.registerListener(mAppWidgetId, this) + mLoader?.startLoading() + } + + /** + * This gets the selection string for the loader. This ends up doing a query in the + * shared preferences. + */ + private fun queryForSelection(): String { + return if (Utils.getHideDeclinedEvents(mContext)) EVENT_SELECTION_HIDE_DECLINED + else EVENT_SELECTION + } + + /** + * @return The uri for the loader + */ + private fun createLoaderUri(): Uri { + val now: Long = System.currentTimeMillis() + // Add a day on either side to catch all-day events + val begin: Long = now - DateUtils.DAY_IN_MILLIS + val end: Long = + now + SEARCH_DURATION + DateUtils.DAY_IN_MILLIS + return Uri.withAppendedPath( + Instances.CONTENT_URI, + begin.toString() + "/" + end + ) + } + + /** + * Calculates and returns the next time we should push widget updates. + */ + private fun calculateUpdateTime( + model: CalendarAppWidgetModel, + now: Long, + timeZone: String + ): Long { + // Make sure an update happens at midnight or earlier + var minUpdateTime = getNextMidnightTimeMillis(timeZone) + for (event in model.mEventInfos) { + val start: Long + val end: Long + start = event.start + end = event.end + + // We want to update widget when we enter/exit time range of an event. + if (now < start) { + minUpdateTime = Math.min(minUpdateTime, start) + } else if (now < end) { + minUpdateTime = Math.min(minUpdateTime, end) + } + } + return minUpdateTime + } + + /* + * (non-Javadoc) + * @see + * android.content.Loader.OnLoadCompleteListener#onLoadComplete(android + * .content.Loader, java.lang.Object) + */ + @Override + override fun onLoadComplete(loader: Loader<Cursor?>?, cursor: Cursor?) { + if (cursor == null) { + return + } + // If a newer update has happened since we started clean up and + // return + synchronized(mLock) { + if (cursor.isClosed()) { + Log.wtf(TAG, "Got a closed cursor from onLoadComplete") + return + } + if (mLastSerialNum != mSerialNum) { + return + } + val now: Long = System.currentTimeMillis() + val tz: String? = Utils.getTimeZone(mContext, mTimezoneChanged) + + // Copy it to a local static cursor. + val matrixCursor: MatrixCursor? = Utils.matrixCursorFromCursor(cursor) + try { + mModel = buildAppWidgetModel(mContext, matrixCursor, tz) + } finally { + if (matrixCursor != null) { + matrixCursor?.close() + } + if (cursor != null) { + cursor?.close() + } + } + + // Schedule an alarm to wake ourselves up for the next update. + // We also cancel + // all existing wake-ups because PendingIntents don't match + // against extras. + var triggerTime = calculateUpdateTime(mModel as CalendarAppWidgetModel, + now, tz as String) + + // If no next-update calculated, or bad trigger time in past, + // schedule + // update about six hours from now. + if (triggerTime < now) { + Log.w(TAG, "Encountered bad trigger time " + formatDebugTime(triggerTime, now)) + triggerTime = now + UPDATE_TIME_NO_EVENTS + } + val alertManager: AlarmManager = mContext + ?.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val pendingUpdate: PendingIntent = CalendarAppWidgetProvider + .getUpdateIntent(mContext) + alertManager.cancel(pendingUpdate) + alertManager.set(AlarmManager.RTC, triggerTime, pendingUpdate) + val time = Time(Utils.getTimeZone(mContext, null)) + time.setToNow() + if (time.normalize(true) !== sLastUpdateTime) { + val time2 = Time(Utils.getTimeZone(mContext, null)) + time2.set(sLastUpdateTime) + time2.normalize(true) + if (time.year !== time2.year || time.yearDay !== time2.yearDay) { + val updateIntent = Intent( + Utils.getWidgetUpdateAction(mContext as Context) + ) + mContext?.sendBroadcast(updateIntent) + } + sLastUpdateTime = time.toMillis(true) + } + val widgetManager: AppWidgetManager = AppWidgetManager.getInstance(mContext) + if (widgetManager == null) { + return + } + if (mAppWidgetId == -1) { + val ids: IntArray = widgetManager.getAppWidgetIds( + CalendarAppWidgetProvider + .getComponentName(mContext) + ) + widgetManager.notifyAppWidgetViewDataChanged(ids, R.id.events_list) + } else { + widgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.events_list) + } + } + } + + @Override + override fun onReceive(context: Context?, intent: Intent) { + if (LOGD) Log.d(TAG, "AppWidgetService received an intent. It was " + intent.toString()) + mContext = context + + // We cannot do any queries from the UI thread, so push the 'selection' query + // to a background thread. However the implementation of the latter query + // (cursor loading) uses CursorLoader which must be initiated from the UI thread, + // so there is some convoluted handshaking here. + // + // Note that as currently implemented, this must run in a single threaded executor + // or else the loads may be run out of order. + // + // TODO: Remove use of mHandler and CursorLoader, and do all the work synchronously + // in the background thread. All the handshaking going on here between the UI and + // background thread with using goAsync, mHandler, and CursorLoader is confusing. + val result: PendingResult = goAsync() + executor.submit(object : Runnable { + @Override + override fun run() { + // We always complete queryForSelection() even if the load task ends up being + // canceled because of a more recent one. Optimizing this to allow + // canceling would require keeping track of all the PendingResults + // (from goAsync) to abort them. Defer this until it becomes a problem. + val selection = queryForSelection() + if (mLoader == null) { + mAppWidgetId = -1 + mHandler.post(object : Runnable { + @Override + override fun run() { + initLoader(selection) + result.finish() + } + }) + } else { + mHandler.post( + createUpdateLoaderRunnable( + selection, result, + currentVersion.incrementAndGet() + ) + ) + } + } + }) + } + + internal companion object { + private const val LOGD = false + + // Suppress unnecessary logging about update time. Need to be static as this object is + // re-instantiated frequently. + // TODO: It seems loadData() is called via onCreate() four times, which should mean + // unnecessary CalendarFactory object is created and dropped. It is not efficient. + private var sLastUpdateTime = UPDATE_TIME_NO_EVENTS + private var mModel: CalendarAppWidgetModel? = null + private val mLock: Object = Object() + + @Volatile + private var mSerialNum = 0 + private val currentVersion: AtomicInteger = AtomicInteger(0) + + /* @VisibleForTesting */ + @JvmStatic protected fun buildAppWidgetModel( + context: Context?, + cursor: Cursor?, + timeZone: String? + ): CalendarAppWidgetModel { + val model = CalendarAppWidgetModel(context as Context, timeZone) + model.buildFromCursor(cursor as Cursor, timeZone) + return model + } + + @JvmStatic private fun getNextMidnightTimeMillis(timezone: String): Long { + val time = Time() + time.setToNow() + time.monthDay++ + time.hour = 0 + time.minute = 0 + time.second = 0 + val midnightDeviceTz: Long = time.normalize(true) + time.timezone = timezone + time.setToNow() + time.monthDay++ + time.hour = 0 + time.minute = 0 + time.second = 0 + val midnightHomeTz: Long = time.normalize(true) + return Math.min(midnightDeviceTz, midnightHomeTz) + } + + @JvmStatic fun updateTextView( + views: RemoteViews, + id: Int, + visibility: Int, + string: String? + ) { + views.setViewVisibility(id, visibility) + if (visibility == View.VISIBLE) { + views.setTextViewText(id, string) + } + } + } + } +}
\ No newline at end of file |