1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.calendar 17 18 import com.android.calendar.CalendarController.ViewType 19 import android.content.Context 20 import android.os.Handler 21 import android.text.format.DateUtils 22 import android.text.format.Time 23 import android.view.LayoutInflater 24 import android.view.View 25 import android.view.ViewGroup 26 import android.widget.BaseAdapter 27 import android.widget.TextView 28 import java.util.Formatter 29 import java.util.Locale 30 31 /* 32 * The MenuSpinnerAdapter defines the look of the ActionBar's pull down menu 33 * for small screen layouts. The pull down menu replaces the tabs uses for big screen layouts 34 * 35 * The MenuSpinnerAdapter responsible for creating the views used for in the pull down menu. 36 */ 37 class CalendarViewAdapter(context: Context, viewType: Int, showDate: Boolean) : BaseAdapter() { 38 private val mButtonNames: Array<String> // Text on buttons 39 40 // Used to define the look of the menu button according to the current view: 41 // Day view: show day of the week + full date underneath 42 // Week view: show the month + year 43 // Month view: show the month + year 44 // Agenda view: show day of the week + full date underneath 45 private var mCurrentMainView: Int 46 private val mInflater: LayoutInflater 47 48 // The current selected event's time, used to calculate the date and day of the week 49 // for the buttons. 50 private var mMilliTime: Long = 0 51 private var mTimeZone: String? = null 52 private var mTodayJulianDay: Long = 0 53 private val mContext: Context = context 54 private val mFormatter: Formatter 55 private val mStringBuilder: StringBuilder 56 private var mMidnightHandler: Handler? = null // Used to run a time update every midnight 57 private val mShowDate: Boolean // Spinner mode indicator (view name or view name with date) 58 59 // Updates time specific variables (time-zone, today's Julian day). 60 private val mTimeUpdater: Runnable = object : Runnable { 61 @Override runnull62 override fun run() { 63 refresh(mContext) 64 } 65 } 66 67 // Sets the time zone and today's Julian day to be used by the adapter. 68 // Also, notify listener on the change and resets the midnight update thread. refreshnull69 fun refresh(context: Context?) { 70 mTimeZone = Utils.getTimeZone(context, mTimeUpdater) 71 val time = Time(mTimeZone) 72 val now: Long = System.currentTimeMillis() 73 time.set(now) 74 mTodayJulianDay = Time.getJulianDay(now, time.gmtoff).toLong() 75 notifyDataSetChanged() 76 setMidnightHandler() 77 } 78 79 // Sets a thread to run 1 second after midnight and update the current date 80 // This is used to display correctly the date of yesterday/today/tomorrow setMidnightHandlernull81 private fun setMidnightHandler() { 82 mMidnightHandler?.removeCallbacks(mTimeUpdater) 83 // Set the time updater to run at 1 second after midnight 84 val now: Long = System.currentTimeMillis() 85 val time = Time(mTimeZone) 86 time.set(now) 87 val runInMillis: Long = ((24 * 3600 - time.hour * 3600 - time.minute * 60 - 88 time.second + 1) * 1000).toLong() 89 mMidnightHandler?.postDelayed(mTimeUpdater, runInMillis) 90 } 91 92 // Stops the midnight update thread, called by the activity when it is paused. onPausenull93 fun onPause() { 94 mMidnightHandler?.removeCallbacks(mTimeUpdater) 95 } 96 97 // Returns the amount of buttons in the menu 98 @Override getCountnull99 override fun getCount(): Int { 100 return mButtonNames.size 101 } 102 103 @Override getItemnull104 override fun getItem(position: Int): Any? { 105 return if (position < mButtonNames.size) { 106 mButtonNames[position] 107 } else null 108 } 109 110 @Override getItemIdnull111 override fun getItemId(position: Int): Long { 112 // Item ID is its location in the list 113 return position.toLong() 114 } 115 116 @Override hasStableIdsnull117 override fun hasStableIds(): Boolean { 118 return false 119 } 120 121 @Override getViewnull122 override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { 123 var v: View? 124 if (mShowDate) { 125 // Check if can recycle the view 126 if (convertView == null || (convertView.getTag() as Int) 127 != R.layout.actionbar_pulldown_menu_top_button as Int) { 128 v = mInflater.inflate(R.layout.actionbar_pulldown_menu_top_button, parent, false) 129 // Set the tag to make sure you can recycle it when you get it 130 // as a convert view 131 v.setTag(Integer(R.layout.actionbar_pulldown_menu_top_button)) 132 } else { 133 v = convertView 134 } 135 val weekDay: TextView = v?.findViewById(R.id.top_button_weekday) as TextView 136 val date: TextView = v.findViewById(R.id.top_button_date) as TextView 137 when (mCurrentMainView) { 138 ViewType.DAY -> { 139 weekDay.setVisibility(View.VISIBLE) 140 weekDay.setText(buildDayOfWeek()) 141 date.setText(buildFullDate()) 142 } 143 ViewType.WEEK -> { 144 if (Utils.getShowWeekNumber(mContext)) { 145 weekDay.setVisibility(View.VISIBLE) 146 weekDay.setText(buildWeekNum()) 147 } else { 148 weekDay.setVisibility(View.GONE) 149 } 150 date.setText(buildMonthYearDate()) 151 } 152 ViewType.MONTH -> { 153 weekDay.setVisibility(View.GONE) 154 date.setText(buildMonthYearDate()) 155 } 156 else -> v = null 157 } 158 } else { 159 if (convertView == null || (convertView.getTag() as Int) 160 != R.layout.actionbar_pulldown_menu_top_button_no_date as Int) { 161 v = mInflater.inflate( 162 R.layout.actionbar_pulldown_menu_top_button_no_date, parent, false) 163 // Set the tag to make sure you can recycle it when you get it 164 // as a convert view 165 v.setTag(Integer(R.layout.actionbar_pulldown_menu_top_button_no_date)) 166 } else { 167 v = convertView 168 } 169 val title: TextView? = v as TextView? 170 when (mCurrentMainView) { 171 ViewType.DAY -> title?.setText(mButtonNames[DAY_BUTTON_INDEX]) 172 ViewType.WEEK -> title?.setText(mButtonNames[WEEK_BUTTON_INDEX]) 173 ViewType.MONTH -> title?.setText(mButtonNames[MONTH_BUTTON_INDEX]) 174 else -> v = null 175 } 176 } 177 return v 178 } 179 180 @Override getItemViewTypenull181 override fun getItemViewType(position: Int): Int { 182 // Only one kind of view is used 183 return BUTTON_VIEW_TYPE 184 } 185 186 @Override getViewTypeCountnull187 override fun getViewTypeCount(): Int { 188 return VIEW_TYPE_NUM 189 } 190 191 @Override isEmptynull192 override fun isEmpty(): Boolean { 193 return mButtonNames.size == 0 194 } 195 196 @Override getDropDownViewnull197 override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View? { 198 var v: View? = mInflater.inflate(R.layout.actionbar_pulldown_menu_button, parent, false) 199 val viewType: TextView? = v?.findViewById(R.id.button_view) as? TextView 200 val date: TextView? = v?.findViewById(R.id.button_date) as? TextView 201 when (position) { 202 DAY_BUTTON_INDEX -> { 203 viewType?.setText(mButtonNames[DAY_BUTTON_INDEX]) 204 if (mShowDate) { 205 date?.setText(buildMonthDayDate()) 206 } 207 } 208 WEEK_BUTTON_INDEX -> { 209 viewType?.setText(mButtonNames[WEEK_BUTTON_INDEX]) 210 if (mShowDate) { 211 date?.setText(buildWeekDate()) 212 } 213 } 214 MONTH_BUTTON_INDEX -> { 215 viewType?.setText(mButtonNames[MONTH_BUTTON_INDEX]) 216 if (mShowDate) { 217 date?.setText(buildMonthDate()) 218 } 219 } 220 else -> v = convertView 221 } 222 return v 223 } 224 225 // Updates the current viewType 226 // Used to match the label on the menu button with the calendar view setMainViewnull227 fun setMainView(viewType: Int) { 228 mCurrentMainView = viewType 229 notifyDataSetChanged() 230 } 231 232 // Update the date that is displayed on buttons 233 // Used when the user selects a new day/week/month to watch setTimenull234 fun setTime(time: Long) { 235 mMilliTime = time 236 notifyDataSetChanged() 237 } 238 239 // Builds a string with the day of the week and the word yesterday/today/tomorrow 240 // before it if applicable. buildDayOfWeeknull241 private fun buildDayOfWeek(): String { 242 val t = Time(mTimeZone) 243 t.set(mMilliTime) 244 val julianDay: Long = Time.getJulianDay(mMilliTime, t.gmtoff).toLong() 245 var dayOfWeek: String? = null 246 mStringBuilder.setLength(0) 247 dayOfWeek = if (julianDay == mTodayJulianDay) { 248 mContext.getString(R.string.agenda_today, 249 DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, 250 DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString()) 251 } else if (julianDay == mTodayJulianDay - 1) { 252 mContext.getString(R.string.agenda_yesterday, 253 DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, 254 DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString()) 255 } else if (julianDay == mTodayJulianDay + 1) { 256 mContext.getString(R.string.agenda_tomorrow, 257 DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, 258 DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString()) 259 } else { 260 DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, 261 DateUtils.FORMAT_SHOW_WEEKDAY, mTimeZone).toString() 262 } 263 return dayOfWeek.toUpperCase() 264 } 265 266 // Builds strings with different formats: 267 // Full date: Month,day Year 268 // Month year 269 // Month day 270 // Month 271 // Week: month day-day or month day - month day buildFullDatenull272 private fun buildFullDate(): String { 273 mStringBuilder.setLength(0) 274 return DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, 275 DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR, mTimeZone).toString() 276 } 277 buildMonthYearDatenull278 private fun buildMonthYearDate(): String { 279 mStringBuilder.setLength(0) 280 return DateUtils.formatDateRange( 281 mContext, 282 mFormatter, 283 mMilliTime, 284 mMilliTime, 285 DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_MONTH_DAY 286 or DateUtils.FORMAT_SHOW_YEAR, mTimeZone).toString() 287 } 288 buildMonthDayDatenull289 private fun buildMonthDayDate(): String { 290 mStringBuilder.setLength(0) 291 return DateUtils.formatDateRange(mContext, mFormatter, mMilliTime, mMilliTime, 292 DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR, mTimeZone).toString() 293 } 294 buildMonthDatenull295 private fun buildMonthDate(): String { 296 mStringBuilder.setLength(0) 297 return DateUtils.formatDateRange( 298 mContext, 299 mFormatter, 300 mMilliTime, 301 mMilliTime, 302 DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR 303 or DateUtils.FORMAT_NO_MONTH_DAY, mTimeZone).toString() 304 } 305 buildWeekDatenull306 private fun buildWeekDate(): String { 307 // Calculate the start of the week, taking into account the "first day of the week" 308 // setting. 309 val t = Time(mTimeZone) 310 t.set(mMilliTime) 311 val firstDayOfWeek: Int = Utils.getFirstDayOfWeek(mContext) 312 val dayOfWeek: Int = t.weekDay 313 var diff = dayOfWeek - firstDayOfWeek 314 if (diff != 0) { 315 if (diff < 0) { 316 diff += 7 317 } 318 t.monthDay -= diff 319 t.normalize(true /* ignore isDst */) 320 } 321 val weekStartTime: Long = t.toMillis(true) 322 // The end of the week is 6 days after the start of the week 323 val weekEndTime: Long = weekStartTime + DateUtils.WEEK_IN_MILLIS - DateUtils.DAY_IN_MILLIS 324 325 // If week start and end is in 2 different months, use short months names 326 val t1 = Time(mTimeZone) 327 t.set(weekEndTime) 328 var flags: Int = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_YEAR 329 if (t.month !== t1.month) { 330 flags = flags or DateUtils.FORMAT_ABBREV_MONTH 331 } 332 mStringBuilder.setLength(0) 333 return DateUtils.formatDateRange(mContext, mFormatter, weekStartTime, 334 weekEndTime, flags, mTimeZone).toString() 335 } 336 buildWeekNumnull337 private fun buildWeekNum(): String { 338 val week: Int = Utils.getWeekNumberFromTime(mMilliTime, mContext) 339 return mContext.getResources().getQuantityString(R.plurals.weekN, week, week) 340 } 341 342 companion object { 343 private const val TAG = "MenuSpinnerAdapter" 344 345 // Defines the types of view returned by this spinner 346 private const val BUTTON_VIEW_TYPE = 0 347 const val VIEW_TYPE_NUM = 1 // Increase this if you add more view types 348 const val DAY_BUTTON_INDEX = 0 349 const val WEEK_BUTTON_INDEX = 1 350 const val MONTH_BUTTON_INDEX = 2 351 const val AGENDA_BUTTON_INDEX = 3 352 } 353 <lambda>null354 init { 355 mMidnightHandler = Handler() 356 mCurrentMainView = viewType 357 mShowDate = showDate 358 359 // Initialize 360 mButtonNames = context.getResources().getStringArray(R.array.buttons_list) 361 mInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 362 mStringBuilder = StringBuilder(50) 363 mFormatter = Formatter(mStringBuilder, Locale.getDefault()) 364 365 // Sets time specific variables and starts a thread for midnight updates 366 if (showDate) { 367 refresh(context) 368 } 369 } 370 }