summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-06-15 21:48:40 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-06-15 21:48:40 +0000
commit89db82f7b38b6aa7cb00d1f254bf53b3d506f495 (patch)
tree833eb2134f58a28ecd78f54e112e753684509ad7
parent1fccef4e6f4dab82dda7d4dca7c8e8ef736c11d7 (diff)
parent5729362442e88937f0d28853772c4b9cee5feddf (diff)
downloadCalendar-android12-mainline-tzdata3-release.tar.gz
Change-Id: I2a26c5ce71d74fc7586f63170449e41942bfe402
-rw-r--r--Android.bp56
-rw-r--r--Android.mk53
-rw-r--r--AndroidManifest.xml3
-rw-r--r--src/com/android/calendar/AllInOneActivity.java1062
-rw-r--r--src/com/android/calendar/AllInOneActivity.kt1065
-rw-r--r--src/com/android/calendar/AsyncQueryServiceHelper.java70
-rw-r--r--src/com/android/calendar/AsyncQueryServiceHelper.kt61
-rw-r--r--src/com/android/calendar/CalendarApplication.java (renamed from src/com/android/calendar/CalendarApplication.kt)18
-rw-r--r--src/com/android/calendar/CalendarBackupAgent.java41
-rw-r--r--src/com/android/calendar/CalendarBackupAgent.kt40
-rw-r--r--src/com/android/calendar/CalendarController.java713
-rw-r--r--src/com/android/calendar/CalendarController.kt743
-rw-r--r--src/com/android/calendar/CalendarData.java (renamed from src/com/android/calendar/CalendarData.kt)25
-rw-r--r--src/com/android/calendar/CalendarUtils.java356
-rw-r--r--src/com/android/calendar/CalendarUtils.kt354
-rw-r--r--src/com/android/calendar/CalendarViewAdapter.java409
-rw-r--r--src/com/android/calendar/CalendarViewAdapter.kt370
-rw-r--r--src/com/android/calendar/DayFragment.java256
-rw-r--r--src/com/android/calendar/DayFragment.kt233
-rw-r--r--src/com/android/calendar/DayOfMonthDrawable.java77
-rw-r--r--src/com/android/calendar/DayOfMonthDrawable.kt74
-rw-r--r--src/com/android/calendar/DayView.java4008
-rw-r--r--src/com/android/calendar/DayView.kt3990
-rw-r--r--src/com/android/calendar/Event.java642
-rw-r--r--src/com/android/calendar/Event.kt640
-rw-r--r--src/com/android/calendar/EventGeometry.java170
-rw-r--r--src/com/android/calendar/EventGeometry.kt154
-rw-r--r--src/com/android/calendar/EventInfoActivity.java190
-rw-r--r--src/com/android/calendar/EventInfoActivity.kt196
-rw-r--r--src/com/android/calendar/EventInfoFragment.java877
-rw-r--r--src/com/android/calendar/EventInfoFragment.kt787
-rw-r--r--src/com/android/calendar/EventLoader.java286
-rw-r--r--src/com/android/calendar/EventLoader.kt283
-rw-r--r--src/com/android/calendar/GeneralPreferences.java400
-rw-r--r--src/com/android/calendar/GeneralPreferences.kt378
-rw-r--r--src/com/android/calendar/GoogleCalendarUriIntentFilter.java (renamed from src/com/android/calendar/GoogleCalendarUriIntentFilter.kt)32
-rw-r--r--src/com/android/calendar/MultiStateButton.java192
-rw-r--r--src/com/android/calendar/MultiStateButton.kt166
-rw-r--r--src/com/android/calendar/OtherPreferences.java210
-rw-r--r--src/com/android/calendar/OtherPreferences.kt184
-rw-r--r--src/com/android/calendar/StickyHeaderListView.java395
-rw-r--r--src/com/android/calendar/StickyHeaderListView.kt386
-rw-r--r--src/com/android/calendar/UpgradeReceiver.java (renamed from src/com/android/calendar/UpgradeReceiver.kt)19
-rw-r--r--src/com/android/calendar/Utils.java1499
-rw-r--r--src/com/android/calendar/Utils.kt1577
-rw-r--r--src/com/android/calendar/alerts/AlarmManagerInterface.java (renamed from src/com/android/calendar/alerts/AlarmManagerInterface.kt)13
-rw-r--r--src/com/android/calendar/alerts/AlarmScheduler.java322
-rw-r--r--src/com/android/calendar/alerts/AlarmScheduler.kt352
-rw-r--r--src/com/android/calendar/alerts/AlertReceiver.java123
-rw-r--r--src/com/android/calendar/alerts/AlertReceiver.kt118
-rw-r--r--src/com/android/calendar/alerts/AlertService.java202
-rw-r--r--src/com/android/calendar/alerts/AlertService.kt167
-rw-r--r--src/com/android/calendar/alerts/AlertUtils.java108
-rw-r--r--src/com/android/calendar/alerts/AlertUtils.kt92
-rw-r--r--src/com/android/calendar/alerts/DismissAlarmsService.java136
-rw-r--r--src/com/android/calendar/alerts/DismissAlarmsService.kt127
-rw-r--r--src/com/android/calendar/alerts/GlobalDismissManager.java84
-rw-r--r--src/com/android/calendar/alerts/GlobalDismissManager.kt58
-rw-r--r--src/com/android/calendar/alerts/InitAlarmsService.java62
-rw-r--r--src/com/android/calendar/alerts/InitAlarmsService.kt62
-rw-r--r--src/com/android/calendar/alerts/NotificationMgr.java (renamed from src/com/android/calendar/alerts/NotificationMgr.kt)25
-rw-r--r--src/com/android/calendar/alerts/QuickResponseActivity.java108
-rw-r--r--src/com/android/calendar/alerts/QuickResponseActivity.kt96
-rw-r--r--src/com/android/calendar/month/MonthByWeekAdapter.java406
-rw-r--r--src/com/android/calendar/month/MonthByWeekAdapter.kt406
-rw-r--r--src/com/android/calendar/month/MonthByWeekFragment.java494
-rw-r--r--src/com/android/calendar/month/MonthByWeekFragment.kt497
-rw-r--r--src/com/android/calendar/month/MonthListView.java51
-rw-r--r--src/com/android/calendar/month/MonthListView.kt43
-rw-r--r--src/com/android/calendar/month/MonthWeekEventsView.java1110
-rw-r--r--src/com/android/calendar/month/MonthWeekEventsView.kt1061
-rw-r--r--src/com/android/calendar/month/SimpleDayPickerFragment.java612
-rw-r--r--src/com/android/calendar/month/SimpleDayPickerFragment.kt616
-rw-r--r--src/com/android/calendar/month/SimpleWeekView.java551
-rw-r--r--src/com/android/calendar/month/SimpleWeekView.kt563
-rw-r--r--src/com/android/calendar/month/SimpleWeeksAdapter.java302
-rw-r--r--src/com/android/calendar/month/SimpleWeeksAdapter.kt314
-rw-r--r--src/com/android/calendar/widget/CalendarAppWidgetModel.java430
-rw-r--r--src/com/android/calendar/widget/CalendarAppWidgetModel.kt409
-rw-r--r--src/com/android/calendar/widget/CalendarAppWidgetProvider.java230
-rw-r--r--src/com/android/calendar/widget/CalendarAppWidgetProvider.kt251
-rw-r--r--src/com/android/calendar/widget/CalendarAppWidgetService.java626
-rw-r--r--src/com/android/calendar/widget/CalendarAppWidgetService.kt665
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