diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-06-15 21:48:40 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-06-15 21:48:40 +0000 |
commit | 89db82f7b38b6aa7cb00d1f254bf53b3d506f495 (patch) | |
tree | 833eb2134f58a28ecd78f54e112e753684509ad7 | |
parent | 1fccef4e6f4dab82dda7d4dca7c8e8ef736c11d7 (diff) | |
parent | 5729362442e88937f0d28853772c4b9cee5feddf (diff) | |
download | Calendar-android12-mainline-tzdata3-release.tar.gz |
Snap for 8730993 from 5729362442e88937f0d28853772c4b9cee5feddf to mainline-tzdata3-releaseaml_tz3_314012070aml_tz3_314012050aml_tz3_314012010aml_tz3_313110000aml_tz3_312511020aml_tz3_312511010aml_tz3_312410020aml_tz3_312410010android12-mainline-tzdata3-releaseaml_tz3_314012010
Change-Id: I2a26c5ce71d74fc7586f63170449e41942bfe402
83 files changed, 17935 insertions, 17697 deletions
diff --git a/Android.bp b/Android.bp deleted file mode 100644 index 3c6477b3..00000000 --- a/Android.bp +++ /dev/null @@ -1,56 +0,0 @@ -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 new file mode 100644 index 00000000..dce26a46 --- /dev/null +++ b/Android.mk @@ -0,0 +1,53 @@ +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 fed61b0c..c9c5a04b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -38,8 +38,7 @@ <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-permission android:name="android.permission.POST_NOTIFICATIONS" /> - <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="30"></uses-sdk> + <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="29"></uses-sdk> <application android:name="CalendarApplication" diff --git a/src/com/android/calendar/AllInOneActivity.java b/src/com/android/calendar/AllInOneActivity.java new file mode 100644 index 00000000..cec6a40f --- /dev/null +++ b/src/com/android/calendar/AllInOneActivity.java @@ -0,0 +1,1062 @@ +/* + * 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 deleted file mode 100644 index 6c2e825f..00000000 --- a/src/com/android/calendar/AllInOneActivity.kt +++ /dev/null @@ -1,1065 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..c6e0a2bc --- /dev/null +++ b/src/com/android/calendar/AsyncQueryServiceHelper.java @@ -0,0 +1,70 @@ +/* + * 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 deleted file mode 100644 index 47973304..00000000 --- a/src/com/android/calendar/AsyncQueryServiceHelper.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.kt b/src/com/android/calendar/CalendarApplication.java index 445d7257..d0ca4698 100644 --- a/src/com/android/calendar/CalendarApplication.kt +++ b/src/com/android/calendar/CalendarApplication.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -13,18 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.calendar -import android.app.Application +package com.android.calendar; -class CalendarApplication : Application() { - override fun onCreate() { - super.onCreate() +import android.app.Application; + +public class CalendarApplication extends Application { + @Override + public void 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 new file mode 100644 index 00000000..02456fdc --- /dev/null +++ b/src/com/android/calendar/CalendarBackupAgent.java @@ -0,0 +1,41 @@ +/* + * 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 deleted file mode 100644 index f3e230ac..00000000 --- a/src/com/android/calendar/CalendarBackupAgent.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..37286f2e --- /dev/null +++ b/src/com/android/calendar/CalendarController.java @@ -0,0 +1,713 @@ +/* + * 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 deleted file mode 100644 index 16ee8fdd..00000000 --- a/src/com/android/calendar/CalendarController.kt +++ /dev/null @@ -1,743 +0,0 @@ -/* - * 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.kt b/src/com/android/calendar/CalendarData.java index 7370f2e2..5c8456fa 100644 --- a/src/com/android/calendar/CalendarData.kt +++ b/src/com/android/calendar/CalendarData.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -13,17 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -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") +package com.android.calendar; - @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 +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" }; +} diff --git a/src/com/android/calendar/CalendarUtils.java b/src/com/android/calendar/CalendarUtils.java new file mode 100644 index 00000000..0238c321 --- /dev/null +++ b/src/com/android/calendar/CalendarUtils.java @@ -0,0 +1,356 @@ +/* + * 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 deleted file mode 100644 index 94ca7234..00000000 --- a/src/com/android/calendar/CalendarUtils.kt +++ /dev/null @@ -1,354 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..524268fc --- /dev/null +++ b/src/com/android/calendar/CalendarViewAdapter.java @@ -0,0 +1,409 @@ +/* + * 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 deleted file mode 100644 index 2fe10272..00000000 --- a/src/com/android/calendar/CalendarViewAdapter.kt +++ /dev/null @@ -1,370 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..a9fb39ed --- /dev/null +++ b/src/com/android/calendar/DayFragment.java @@ -0,0 +1,256 @@ +/* + * 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 deleted file mode 100644 index 39e92f5b..00000000 --- a/src/com/android/calendar/DayFragment.kt +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..461ab317 --- /dev/null +++ b/src/com/android/calendar/DayOfMonthDrawable.java @@ -0,0 +1,77 @@ +/* + * 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 deleted file mode 100644 index e348b5a2..00000000 --- a/src/com/android/calendar/DayOfMonthDrawable.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..2fc00b3c --- /dev/null +++ b/src/com/android/calendar/DayView.java @@ -0,0 +1,4008 @@ +/* + * 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 deleted file mode 100644 index 42621638..00000000 --- a/src/com/android/calendar/DayView.kt +++ /dev/null @@ -1,3990 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..095e43e7 --- /dev/null +++ b/src/com/android/calendar/Event.java @@ -0,0 +1,642 @@ +/* + * 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 deleted file mode 100644 index c21a0a0e..00000000 --- a/src/com/android/calendar/Event.kt +++ /dev/null @@ -1,640 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..cdecb49c --- /dev/null +++ b/src/com/android/calendar/EventGeometry.java @@ -0,0 +1,170 @@ +/* + * 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 deleted file mode 100644 index 43fc3e77..00000000 --- a/src/com/android/calendar/EventGeometry.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..626e099d --- /dev/null +++ b/src/com/android/calendar/EventInfoActivity.java @@ -0,0 +1,190 @@ +/* + * 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 deleted file mode 100644 index c0a1b9cd..00000000 --- a/src/com/android/calendar/EventInfoActivity.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..0aa83d02 --- /dev/null +++ b/src/com/android/calendar/EventInfoFragment.java @@ -0,0 +1,877 @@ +/* + * 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 deleted file mode 100644 index fcc27fc8..00000000 --- a/src/com/android/calendar/EventInfoFragment.kt +++ /dev/null @@ -1,787 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..d34b1c7c --- /dev/null +++ b/src/com/android/calendar/EventLoader.java @@ -0,0 +1,286 @@ +/* + * 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 deleted file mode 100644 index a05e8a2e..00000000 --- a/src/com/android/calendar/EventLoader.kt +++ /dev/null @@ -1,283 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..a42f07e3 --- /dev/null +++ b/src/com/android/calendar/GeneralPreferences.java @@ -0,0 +1,400 @@ +/* + * 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 deleted file mode 100644 index dd4c9550..00000000 --- a/src/com/android/calendar/GeneralPreferences.kt +++ /dev/null @@ -1,378 +0,0 @@ -/* - * 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.kt b/src/com/android/calendar/GoogleCalendarUriIntentFilter.java index d2fe77f9..3970115b 100644 --- a/src/com/android/calendar/GoogleCalendarUriIntentFilter.kt +++ b/src/com/android/calendar/GoogleCalendarUriIntentFilter.java @@ -1,5 +1,6 @@ /* -** Copyright 2021, The Android Open Source Project +** +** Copyright 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. @@ -13,25 +14,28 @@ ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ** limitations under the License. */ -package com.android.calendar -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle +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); -class GoogleCalendarUriIntentFilter : Activity() { - protected override fun onCreate(icicle: Bundle?) { - super.onCreate(icicle) - val intent: Intent = getIntent() + Intent intent = getIntent(); if (intent != null) { // Pass it on to the next Activity. try { - startNextMatchingActivity(intent) - } catch (ex: ActivityNotFoundException) { + startNextMatchingActivity(intent); + } catch (ActivityNotFoundException ex) { // 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 new file mode 100644 index 00000000..8034b28e --- /dev/null +++ b/src/com/android/calendar/MultiStateButton.java @@ -0,0 +1,192 @@ +/* + * 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 deleted file mode 100644 index f86ee6ba..00000000 --- a/src/com/android/calendar/MultiStateButton.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..a59d3f46 --- /dev/null +++ b/src/com/android/calendar/OtherPreferences.java @@ -0,0 +1,210 @@ +/* + * 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 deleted file mode 100644 index f1507ccf..00000000 --- a/src/com/android/calendar/OtherPreferences.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..981e7af7 --- /dev/null +++ b/src/com/android/calendar/StickyHeaderListView.java @@ -0,0 +1,395 @@ +/* + * 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 deleted file mode 100644 index 37733b7b..00000000 --- a/src/com/android/calendar/StickyHeaderListView.kt +++ /dev/null @@ -1,386 +0,0 @@ -/* - * 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.kt b/src/com/android/calendar/UpgradeReceiver.java index ab2de1de..0e89286d 100644 --- a/src/com/android/calendar/UpgradeReceiver.kt +++ b/src/com/android/calendar/UpgradeReceiver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.calendar -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent +package com.android.calendar; -class UpgradeReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - Utils.trySyncAndDisableUpgradeReceiver(context) +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); } + }
\ No newline at end of file diff --git a/src/com/android/calendar/Utils.java b/src/com/android/calendar/Utils.java new file mode 100644 index 00000000..cc55c999 --- /dev/null +++ b/src/com/android/calendar/Utils.java @@ -0,0 +1,1499 @@ +/* + * 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 deleted file mode 100644 index ef780485..00000000 --- a/src/com/android/calendar/Utils.kt +++ /dev/null @@ -1,1577 +0,0 @@ -/* - * 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.kt b/src/com/android/calendar/alerts/AlarmManagerInterface.java index be9d86f2..3c66434d 100644 --- a/src/com/android/calendar/alerts/AlarmManagerInterface.kt +++ b/src/com/android/calendar/alerts/AlarmManagerInterface.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.calendar.alerts -import android.app.PendingIntent +package com.android.calendar.alerts; + +import android.app.PendingIntent; /** * AlarmManager abstracted to an interface for testability. */ -interface AlarmManagerInterface { - operator fun set(type: Int, triggerAtMillis: Long, operation: PendingIntent?) -}
\ No newline at end of file +public interface AlarmManagerInterface { + public void set(int type, long triggerAtMillis, PendingIntent operation); +} diff --git a/src/com/android/calendar/alerts/AlarmScheduler.java b/src/com/android/calendar/alerts/AlarmScheduler.java new file mode 100644 index 00000000..97828229 --- /dev/null +++ b/src/com/android/calendar/alerts/AlarmScheduler.java @@ -0,0 +1,322 @@ +/* + * 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 deleted file mode 100644 index c93bbb04..00000000 --- a/src/com/android/calendar/alerts/AlarmScheduler.kt +++ /dev/null @@ -1,352 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..ce80cae1 --- /dev/null +++ b/src/com/android/calendar/alerts/AlertReceiver.java @@ -0,0 +1,123 @@ +/* + * 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 deleted file mode 100644 index 21afa90c..00000000 --- a/src/com/android/calendar/alerts/AlertReceiver.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..d2c994da --- /dev/null +++ b/src/com/android/calendar/alerts/AlertService.java @@ -0,0 +1,202 @@ +/* + * 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 deleted file mode 100644 index bc1b4e04..00000000 --- a/src/com/android/calendar/alerts/AlertService.kt +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..b9aaec29 --- /dev/null +++ b/src/com/android/calendar/alerts/AlertUtils.java @@ -0,0 +1,108 @@ +/* + * 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 deleted file mode 100644 index 18b7e7d1..00000000 --- a/src/com/android/calendar/alerts/AlertUtils.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..1ec3c22d --- /dev/null +++ b/src/com/android/calendar/alerts/DismissAlarmsService.java @@ -0,0 +1,136 @@ +/* + * 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 deleted file mode 100644 index 88683d3a..00000000 --- a/src/com/android/calendar/alerts/DismissAlarmsService.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..27b3e162 --- /dev/null +++ b/src/com/android/calendar/alerts/GlobalDismissManager.java @@ -0,0 +1,84 @@ +/* + * 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 deleted file mode 100644 index 4cf0bc0c..00000000 --- a/src/com/android/calendar/alerts/GlobalDismissManager.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..3a9b0b2c --- /dev/null +++ b/src/com/android/calendar/alerts/InitAlarmsService.java @@ -0,0 +1,62 @@ +/* + * 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 deleted file mode 100644 index 0ac8a474..00000000 --- a/src/com/android/calendar/alerts/InitAlarmsService.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.kt b/src/com/android/calendar/alerts/NotificationMgr.java index 609b8141..0ab475c3 100644 --- a/src/com/android/calendar/alerts/NotificationMgr.kt +++ b/src/com/android/calendar/alerts/NotificationMgr.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -13,28 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.calendar.alerts -import com.android.calendar.alerts.AlertService.NotificationWrapper +package com.android.calendar.alerts; -abstract class NotificationMgr { - abstract fun notify(id: Int, notification: NotificationWrapper?) - abstract fun cancel(id: Int) +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); /** * Don't actually use the notification framework's cancelAll since the SyncAdapter * might post notifications and we don't want to affect those. */ - fun cancelAll() { - cancelAllBetween(0, AlertService.MAX_NOTIFICATIONS) + public void cancelAll() { + cancelAllBetween(0, AlertService.MAX_NOTIFICATIONS); } /** * Cancels IDs between the specified bounds, inclusively. */ - fun cancelAllBetween(from: Int, to: Int) { - for (i in from..to) { - cancel(i) + public void cancelAllBetween(int from, int to) { + for (int i = from; i <= to; i++) { + 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 new file mode 100644 index 00000000..3d291d02 --- /dev/null +++ b/src/com/android/calendar/alerts/QuickResponseActivity.java @@ -0,0 +1,108 @@ +/* + * 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 deleted file mode 100644 index afccaffd..00000000 --- a/src/com/android/calendar/alerts/QuickResponseActivity.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..45a1bea1 --- /dev/null +++ b/src/com/android/calendar/month/MonthByWeekAdapter.java @@ -0,0 +1,406 @@ +/* + * 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 deleted file mode 100644 index c67b3562..00000000 --- a/src/com/android/calendar/month/MonthByWeekAdapter.kt +++ /dev/null @@ -1,406 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..f8a518d3 --- /dev/null +++ b/src/com/android/calendar/month/MonthByWeekFragment.java @@ -0,0 +1,494 @@ +/* + * 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 deleted file mode 100644 index 9fe9fe49..00000000 --- a/src/com/android/calendar/month/MonthByWeekFragment.kt +++ /dev/null @@ -1,497 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..f2621ccb --- /dev/null +++ b/src/com/android/calendar/month/MonthListView.java @@ -0,0 +1,51 @@ +/* + * 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 deleted file mode 100644 index 1facb4c0..00000000 --- a/src/com/android/calendar/month/MonthListView.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..e1c78c67 --- /dev/null +++ b/src/com/android/calendar/month/MonthWeekEventsView.java @@ -0,0 +1,1110 @@ +/* + * 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 deleted file mode 100644 index e4b15494..00000000 --- a/src/com/android/calendar/month/MonthWeekEventsView.kt +++ /dev/null @@ -1,1061 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..2efae6a9 --- /dev/null +++ b/src/com/android/calendar/month/SimpleDayPickerFragment.java @@ -0,0 +1,612 @@ +/* + * 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 deleted file mode 100644 index 01fcbac6..00000000 --- a/src/com/android/calendar/month/SimpleDayPickerFragment.kt +++ /dev/null @@ -1,616 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..4d0c09f4 --- /dev/null +++ b/src/com/android/calendar/month/SimpleWeekView.java @@ -0,0 +1,551 @@ +/* + * 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 deleted file mode 100644 index 4d1298d4..00000000 --- a/src/com/android/calendar/month/SimpleWeekView.kt +++ /dev/null @@ -1,563 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..d29b2622 --- /dev/null +++ b/src/com/android/calendar/month/SimpleWeeksAdapter.java @@ -0,0 +1,302 @@ +/* + * 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 deleted file mode 100644 index 164f05c5..00000000 --- a/src/com/android/calendar/month/SimpleWeeksAdapter.kt +++ /dev/null @@ -1,314 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..a989e18b --- /dev/null +++ b/src/com/android/calendar/widget/CalendarAppWidgetModel.java @@ -0,0 +1,430 @@ +/* + * 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 deleted file mode 100644 index 440d178b..00000000 --- a/src/com/android/calendar/widget/CalendarAppWidgetModel.kt +++ /dev/null @@ -1,409 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..3a69efd3 --- /dev/null +++ b/src/com/android/calendar/widget/CalendarAppWidgetProvider.java @@ -0,0 +1,230 @@ +/* + * 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 deleted file mode 100644 index b3539f22..00000000 --- a/src/com/android/calendar/widget/CalendarAppWidgetProvider.kt +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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 new file mode 100644 index 00000000..ec702c7c --- /dev/null +++ b/src/com/android/calendar/widget/CalendarAppWidgetService.java @@ -0,0 +1,626 @@ +/* + * 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 deleted file mode 100644 index 114fdf12..00000000 --- a/src/com/android/calendar/widget/CalendarAppWidgetService.kt +++ /dev/null @@ -1,665 +0,0 @@ -/* - * 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 |