1 /* 2 * Copyright (C) 2020 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.alarmclock 17 18 import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID 19 import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID 20 import android.content.Context 21 import android.content.Intent 22 import android.content.res.Resources 23 import android.text.format.DateFormat 24 import android.util.TypedValue 25 import android.view.View 26 import android.widget.RemoteViews 27 import android.widget.RemoteViewsService.RemoteViewsFactory 28 29 import com.android.deskclock.LogUtils 30 import com.android.deskclock.R 31 import com.android.deskclock.Utils 32 import com.android.deskclock.data.City 33 import com.android.deskclock.data.DataModel 34 35 import java.util.ArrayList 36 import java.util.Calendar 37 import java.util.Locale 38 import java.util.TimeZone 39 40 /** 41 * This factory produces entries in the world cities list view displayed at the bottom of the 42 * digital widget. Each row is comprised of two world cities located side-by-side. 43 */ 44 class DigitalAppWidgetCityViewsFactory(context: Context, intent: Intent) : RemoteViewsFactory { 45 private val mFillInIntent: Intent = Intent() 46 private val mContext: Context = context 47 private val m12HourFontSize: Float 48 private val m24HourFontSize: Float 49 private val mWidgetId: Int = intent.getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID) 50 private var mFontScale = 1f 51 private var mHomeCity: City? = null 52 private var mShowHomeClock = false 53 private var mCities: List<City>? = emptyList() 54 55 init { 56 val res: Resources = context.getResources() 57 m12HourFontSize = res.getDimension(R.dimen.digital_widget_city_12_medium_font_size) 58 m24HourFontSize = res.getDimension(R.dimen.digital_widget_city_24_medium_font_size) 59 } 60 onCreatenull61 override fun onCreate() { 62 LOGGER.i("DigitalAppWidgetCityViewsFactory onCreate $mWidgetId") 63 } 64 onDestroynull65 override fun onDestroy() { 66 LOGGER.i("DigitalAppWidgetCityViewsFactory onDestroy $mWidgetId") 67 } 68 69 /** 70 * Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and 71 * mShowHomeClock. 72 * 73 * {@inheritDoc} 74 */ 75 @Synchronized getCountnull76 override fun getCount(): Int { 77 val homeClockCount = if (mShowHomeClock) 1 else 0 78 val worldClockCount = mCities!!.size 79 val totalClockCount = homeClockCount + worldClockCount.toDouble() 80 81 // Number of clocks / 2 clocks per row 82 return Math.ceil(totalClockCount / 2).toInt() 83 } 84 85 /** 86 * Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and 87 * mShowHomeClock. 88 * 89 * {@inheritDoc} 90 */ 91 @Synchronized getViewAtnull92 override fun getViewAt(position: Int): RemoteViews { 93 val homeClockOffset = if (mShowHomeClock) -1 else 0 94 val leftIndex = position * 2 + homeClockOffset 95 val rightIndex = leftIndex + 1 96 val left = when { 97 leftIndex == -1 -> mHomeCity 98 leftIndex < mCities!!.size -> mCities!![leftIndex] 99 else -> null 100 } 101 val right = if (rightIndex < mCities!!.size) mCities!![rightIndex] else null 102 val rv = RemoteViews(mContext.getPackageName(), R.layout.world_clock_remote_list_item) 103 104 // Show the left clock if one exists. 105 if (left != null) { 106 update(rv, left, R.id.left_clock, R.id.city_name_left, R.id.city_day_left) 107 } else { 108 hide(rv, R.id.left_clock, R.id.city_name_left, R.id.city_day_left) 109 } 110 111 // Show the right clock if one exists. 112 if (right != null) { 113 update(rv, right, R.id.right_clock, R.id.city_name_right, R.id.city_day_right) 114 } else { 115 hide(rv, R.id.right_clock, R.id.city_name_right, R.id.city_day_right) 116 } 117 118 // Hide last spacer in last row; show for all others. 119 val lastRow = position == count - 1 120 rv.setViewVisibility(R.id.city_spacer, if (lastRow) View.GONE else View.VISIBLE) 121 rv.setOnClickFillInIntent(R.id.widget_item, mFillInIntent) 122 return rv 123 } 124 getItemIdnull125 override fun getItemId(position: Int): Long { 126 return position.toLong() 127 } 128 getLoadingViewnull129 override fun getLoadingView(): RemoteViews? { 130 return null 131 } 132 getViewTypeCountnull133 override fun getViewTypeCount(): Int { 134 return 1 135 } 136 hasStableIdsnull137 override fun hasStableIds(): Boolean { 138 return false 139 } 140 141 /** 142 * Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and 143 * mShowHomeClock. 144 * 145 * {@inheritDoc} 146 */ 147 @Synchronized onDataSetChangednull148 override fun onDataSetChanged() { 149 // Fetch the data on the main Looper. 150 val refreshRunnable = RefreshRunnable() 151 DataModel.dataModel.run(refreshRunnable) 152 153 // Store the data in local variables. 154 mHomeCity = refreshRunnable.mHomeCity 155 mCities = refreshRunnable.mCities 156 mShowHomeClock = refreshRunnable.mShowHomeClock 157 mFontScale = WidgetUtils.getScaleRatio(mContext, null, mWidgetId, mCities!!.size) 158 } 159 updatenull160 private fun update(rv: RemoteViews, city: City, clockId: Int, labelId: Int, dayId: Int) { 161 rv.setCharSequence(clockId, "setFormat12Hour", Utils.get12ModeFormat(0.4f, false)) 162 rv.setCharSequence(clockId, "setFormat24Hour", Utils.get24ModeFormat(false)) 163 164 val is24HourFormat: Boolean = DateFormat.is24HourFormat(mContext) 165 val fontSize = if (is24HourFormat) m24HourFontSize else m12HourFontSize 166 rv.setTextViewTextSize(clockId, TypedValue.COMPLEX_UNIT_PX, fontSize * mFontScale) 167 rv.setString(clockId, "setTimeZone", city.timeZone.id) 168 rv.setTextViewText(labelId, city.name) 169 170 // Compute if the city week day matches the weekday of the current timezone. 171 val localCal = Calendar.getInstance(TimeZone.getDefault()) 172 val cityCal = Calendar.getInstance(city.timeZone) 173 val displayDayOfWeek = localCal[Calendar.DAY_OF_WEEK] != cityCal[Calendar.DAY_OF_WEEK] 174 175 // Bind the week day display. 176 if (displayDayOfWeek) { 177 val locale = Locale.getDefault() 178 val weekday = cityCal.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, locale) 179 val slashDay: String = mContext.getString(R.string.world_day_of_week_label, weekday) 180 rv.setTextViewText(dayId, slashDay) 181 } 182 183 rv.setViewVisibility(dayId, if (displayDayOfWeek) View.VISIBLE else View.GONE) 184 rv.setViewVisibility(clockId, View.VISIBLE) 185 rv.setViewVisibility(labelId, View.VISIBLE) 186 } 187 hidenull188 private fun hide(clock: RemoteViews, clockId: Int, labelId: Int, dayId: Int) { 189 clock.setViewVisibility(dayId, View.INVISIBLE) 190 clock.setViewVisibility(clockId, View.INVISIBLE) 191 clock.setViewVisibility(labelId, View.INVISIBLE) 192 } 193 194 /** 195 * This Runnable fetches data for this factory on the main thread to ensure all DataModel reads 196 * occur on the main thread. 197 */ 198 private class RefreshRunnable : Runnable { 199 var mHomeCity: City? = null 200 var mCities: List<City>? = null 201 var mShowHomeClock = false 202 runnull203 override fun run() { 204 mHomeCity = DataModel.dataModel.homeCity 205 mCities = ArrayList(DataModel.dataModel.selectedCities) 206 mShowHomeClock = DataModel.dataModel.showHomeClock 207 } 208 } 209 210 companion object { 211 private val LOGGER = LogUtils.Logger("DigWidgetViewsFactory") 212 } 213 } 214