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 }