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.widget 17 18 import android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY 19 import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME 20 import android.provider.CalendarContract.EXTRA_EVENT_END_TIME 21 import android.app.AlarmManager 22 import android.app.PendingIntent 23 import android.appwidget.AppWidgetManager 24 import android.appwidget.AppWidgetProvider 25 import android.content.ComponentName 26 import android.content.Context 27 import android.content.Intent 28 import android.net.Uri 29 import android.provider.CalendarContract 30 import android.text.format.DateUtils 31 import android.text.format.Time 32 import android.util.Log 33 import android.widget.RemoteViews 34 import com.android.calendar.AllInOneActivity 35 import com.android.calendar.EventInfoActivity 36 import com.android.calendar.R 37 import com.android.calendar.Utils 38 39 /** 40 * Simple widget to show next upcoming calendar event. 41 */ 42 class CalendarAppWidgetProvider : AppWidgetProvider() { 43 /** 44 * {@inheritDoc} 45 */ 46 @Override onReceivenull47 override fun onReceive(context: Context?, intent: Intent?) { 48 // Handle calendar-specific updates ourselves because they might be 49 // coming in without extras, which AppWidgetProvider then blocks. 50 val action: String? = intent?.getAction() 51 if (LOGD) Log.d(TAG, "AppWidgetProvider got the intent: " + intent.toString()) 52 if (Utils.getWidgetUpdateAction(context as Context).equals(action)) { 53 val appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) 54 performUpdate( 55 context as Context, appWidgetManager, 56 appWidgetManager.getAppWidgetIds(getComponentName(context)), 57 null /* no eventIds */ 58 ) 59 } else if (action != null && (action.equals(Intent.ACTION_PROVIDER_CHANGED) || 60 action.equals(Intent.ACTION_TIME_CHANGED) || 61 action.equals(Intent.ACTION_TIMEZONE_CHANGED) || 62 action.equals(Intent.ACTION_DATE_CHANGED) || 63 action.equals(Utils.getWidgetScheduledUpdateAction(context as Context))) 64 ) { 65 val service = Intent(context, CalendarAppWidgetService::class.java) 66 context.startService(service) 67 } else { 68 super.onReceive(context, intent) 69 } 70 } 71 72 /** 73 * {@inheritDoc} 74 */ 75 @Override onDisablednull76 override fun onDisabled(context: Context) { 77 // Unsubscribe from all AlarmManager updates 78 val am: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 79 val pendingUpdate: PendingIntent = getUpdateIntent(context) 80 am.cancel(pendingUpdate) 81 } 82 83 /** 84 * {@inheritDoc} 85 */ 86 @Override onUpdatenull87 override fun onUpdate( 88 context: Context, 89 appWidgetManager: AppWidgetManager, 90 appWidgetIds: IntArray 91 ) { 92 performUpdate(context, appWidgetManager, 93 appWidgetIds, null /* no eventIds */) 94 } 95 96 /** 97 * Process and push out an update for the given appWidgetIds. This call 98 * actually fires an intent to start [CalendarAppWidgetService] as a 99 * background service which handles the actual update, to prevent ANR'ing 100 * during database queries. 101 * 102 * @param context Context to use when starting [CalendarAppWidgetService]. 103 * @param appWidgetIds List of specific appWidgetIds to update, or null for 104 * all. 105 * @param changedEventIds Specific events known to be changed. If present, 106 * we use it to decide if an update is necessary. 107 */ performUpdatenull108 private fun performUpdate( 109 context: Context, 110 appWidgetManager: AppWidgetManager, 111 appWidgetIds: IntArray, 112 changedEventIds: LongArray? 113 ) { 114 // Launch over to service so it can perform update 115 for (appWidgetId in appWidgetIds) { 116 if (LOGD) Log.d(TAG, "Building widget update...") 117 val updateIntent = Intent(context, CalendarAppWidgetService::class.java) 118 updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) 119 if (changedEventIds != null) { 120 updateIntent.putExtra(EXTRA_EVENT_IDS, changedEventIds) 121 } 122 updateIntent.setData(Uri.parse(updateIntent.toUri(Intent.URI_INTENT_SCHEME))) 123 val views = RemoteViews(context.getPackageName(), R.layout.appwidget) 124 // Calendar header 125 val time = Time(Utils.getTimeZone(context, null)) 126 time.setToNow() 127 val millis: Long = time.toMillis(true) 128 val dayOfWeek: String = DateUtils.getDayOfWeekString( 129 time.weekDay + 1, 130 DateUtils.LENGTH_MEDIUM 131 ) 132 val date: String? = Utils.formatDateRange( 133 context, millis, millis, 134 DateUtils.FORMAT_ABBREV_ALL or DateUtils.FORMAT_SHOW_DATE 135 or DateUtils.FORMAT_NO_YEAR 136 ) 137 views.setTextViewText(R.id.day_of_week, dayOfWeek) 138 views.setTextViewText(R.id.date, date) 139 // Attach to list of events 140 views.setRemoteAdapter(appWidgetId, R.id.events_list, updateIntent) 141 appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.events_list) 142 143 // Launch calendar app when the user taps on the header 144 val launchCalendarIntent = Intent(Intent.ACTION_VIEW) 145 launchCalendarIntent.setClass(context, AllInOneActivity::class.java) 146 launchCalendarIntent 147 .setData(Uri.parse("content://com.android.calendar/time/$millis")) 148 val launchCalendarPendingIntent: PendingIntent = PendingIntent.getActivity( 149 context, 0 /* no requestCode */, launchCalendarIntent, 0 /* no flags */ 150 ) 151 views.setOnClickPendingIntent(R.id.header, launchCalendarPendingIntent) 152 153 // Each list item will call setOnClickExtra() to let the list know 154 // which item 155 // is selected by a user. 156 val updateEventIntent: PendingIntent = getLaunchPendingIntentTemplate(context) 157 views.setPendingIntentTemplate(R.id.events_list, updateEventIntent) 158 appWidgetManager.updateAppWidget(appWidgetId, views) 159 } 160 } 161 162 companion object { 163 const val TAG = "CalendarAppWidgetProvider" 164 const val LOGD = false 165 166 // TODO Move these to Calendar.java 167 const val EXTRA_EVENT_IDS = "com.android.calendar.EXTRA_EVENT_IDS" 168 169 /** 170 * Build [ComponentName] describing this specific 171 * [AppWidgetProvider] 172 */ getComponentNamenull173 @JvmStatic fun getComponentName(context: Context?): ComponentName { 174 return ComponentName(context as Context, CalendarAppWidgetProvider::class.java) 175 } 176 177 /** 178 * Build the [PendingIntent] used to trigger an update of all calendar 179 * widgets. Uses [Utils.getWidgetScheduledUpdateAction] to 180 * directly target all widgets instead of using 181 * [AppWidgetManager.EXTRA_APPWIDGET_IDS]. 182 * 183 * @param context Context to use when building broadcast. 184 */ getUpdateIntentnull185 @JvmStatic fun getUpdateIntent(context: Context?): PendingIntent { 186 val intent = Intent(Utils.getWidgetScheduledUpdateAction(context as Context)) 187 intent.setDataAndType(CalendarContract.CONTENT_URI, Utils.APPWIDGET_DATA_TYPE) 188 return PendingIntent.getBroadcast( 189 context, 0 /* no requestCode */, intent, 190 0 /* no flags */ 191 ) 192 } 193 194 /** 195 * Build a [PendingIntent] to launch the Calendar app. This should be used 196 * in combination with [RemoteViews.setPendingIntentTemplate]. 197 */ getLaunchPendingIntentTemplatenull198 @JvmStatic fun getLaunchPendingIntentTemplate(context: Context?): PendingIntent { 199 val launchIntent = Intent() 200 launchIntent.setAction(Intent.ACTION_VIEW) 201 launchIntent.setFlags( 202 Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or 203 Intent.FLAG_ACTIVITY_TASK_ON_HOME 204 ) 205 launchIntent.setClass(context as Context, AllInOneActivity::class.java) 206 return PendingIntent.getActivity( 207 context, 0 /* no requestCode */, launchIntent, 208 PendingIntent.FLAG_UPDATE_CURRENT 209 ) 210 } 211 212 /** 213 * Build an [Intent] available as FillInIntent to launch the Calendar app. 214 * This should be used in combination with 215 * [RemoteViews.setOnClickFillInIntent]. 216 * If the go to time is 0, then calendar will be launched without a starting time. 217 * 218 * @param goToTime time that calendar should take the user to, or 0 to 219 * indicate no specific start time. 220 */ getLaunchFillInIntentnull221 @JvmStatic fun getLaunchFillInIntent( 222 context: Context?, 223 id: Long, 224 start: Long, 225 end: Long, 226 allDay: Boolean 227 ): Intent { 228 val fillInIntent = Intent() 229 var dataString = "content://com.android.calendar/events" 230 if (id != 0L) { 231 fillInIntent.putExtra(Utils.INTENT_KEY_DETAIL_VIEW, true) 232 fillInIntent.setFlags( 233 Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or 234 Intent.FLAG_ACTIVITY_TASK_ON_HOME 235 ) 236 dataString += "/$id" 237 // If we have an event id - start the event info activity 238 fillInIntent.setClass(context as Context, EventInfoActivity::class.java) 239 } else { 240 // If we do not have an event id - start AllInOne 241 fillInIntent.setClass(context as Context, AllInOneActivity::class.java) 242 } 243 val data: Uri = Uri.parse(dataString) 244 fillInIntent.setData(data) 245 fillInIntent.putExtra(EXTRA_EVENT_BEGIN_TIME, start) 246 fillInIntent.putExtra(EXTRA_EVENT_END_TIME, end) 247 fillInIntent.putExtra(EXTRA_EVENT_ALL_DAY, allDay) 248 return fillInIntent 249 } 250 } 251 }