1 /** 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.inputmethod.dictionarypack; 18 19 import android.app.AlarmManager; 20 import android.app.PendingIntent; 21 import android.app.Service; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.os.IBinder; 25 import android.widget.Toast; 26 27 import com.android.inputmethod.latin.R; 28 29 import java.util.Locale; 30 import java.util.Random; 31 import java.util.concurrent.LinkedBlockingQueue; 32 import java.util.concurrent.ThreadPoolExecutor; 33 import java.util.concurrent.TimeUnit; 34 35 /** 36 * Service that handles background tasks for the dictionary provider. 37 * 38 * This service provides the context for the long-running operations done by the 39 * dictionary provider. Those include: 40 * - Checking for the last update date and scheduling the next update. This runs every 41 * day around midnight, upon reception of the DATE_CHANGED_INTENT_ACTION broadcast. 42 * Every four days, it schedules an update of the metadata with the alarm manager. 43 * - Issuing the order to update the metadata. This runs every four days, between 0 and 44 * 6, upon reception of the UPDATE_NOW_INTENT_ACTION broadcast sent by the alarm manager 45 * as a result of the above action. 46 * - Handling a download that just ended. These come in two flavors: 47 * - Metadata is finished downloading. We should check whether there are new dictionaries 48 * available, and download those that we need that have new versions. 49 * - A dictionary file finished downloading. We should put the file ready for a client IME 50 * to access, and mark the current state as such. 51 */ 52 public final class DictionaryService extends Service { 53 /** 54 * The package name, to use in the intent actions. 55 */ 56 private static final String PACKAGE_NAME = "com.android.inputmethod.latin"; 57 58 /** 59 * The action of the date changing, used to schedule a periodic freshness check 60 */ 61 private static final String DATE_CHANGED_INTENT_ACTION = 62 Intent.ACTION_DATE_CHANGED; 63 64 /** 65 * The action of displaying a toast to warn the user an automatic download is starting. 66 */ 67 /* package */ static final String SHOW_DOWNLOAD_TOAST_INTENT_ACTION = 68 PACKAGE_NAME + ".SHOW_DOWNLOAD_TOAST_INTENT_ACTION"; 69 70 /** 71 * A locale argument, as a String. 72 */ 73 /* package */ static final String LOCALE_INTENT_ARGUMENT = "locale"; 74 75 /** 76 * How often, in milliseconds, we want to update the metadata. This is a 77 * floor value; actually, it may happen several hours later, or even more. 78 */ 79 private static final long UPDATE_FREQUENCY = TimeUnit.DAYS.toMillis(4); 80 81 /** 82 * We are waked around midnight, local time. We want to wake between midnight and 6 am, 83 * roughly. So use a random time between 0 and this delay. 84 */ 85 private static final int MAX_ALARM_DELAY = (int)TimeUnit.HOURS.toMillis(6); 86 87 /** 88 * How long we consider a "very long time". If no update took place in this time, 89 * the content provider will trigger an update in the background. 90 */ 91 private static final long VERY_LONG_TIME = TimeUnit.DAYS.toMillis(14); 92 93 /** 94 * An executor that serializes tasks given to it. 95 */ 96 private ThreadPoolExecutor mExecutor; 97 private static final int WORKER_THREAD_TIMEOUT_SECONDS = 15; 98 99 @Override 100 public void onCreate() { 101 // By default, a thread pool executor does not timeout its core threads, so it will 102 // never kill them when there isn't any work to do any more. That would mean the service 103 // can never die! By creating it this way and calling allowCoreThreadTimeOut, we allow 104 // the single thread to time out after WORKER_THREAD_TIMEOUT_SECONDS = 15 seconds, allowing 105 // the process to be reclaimed by the system any time after that if it's not doing 106 // anything else. 107 // Executors#newSingleThreadExecutor creates a ThreadPoolExecutor but it returns the 108 // superclass ExecutorService which does not have the #allowCoreThreadTimeOut method, 109 // so we can't use that. 110 mExecutor = new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */, 111 WORKER_THREAD_TIMEOUT_SECONDS /* keepAliveTime */, 112 TimeUnit.SECONDS /* unit for keepAliveTime */, 113 new LinkedBlockingQueue<Runnable>() /* workQueue */); 114 mExecutor.allowCoreThreadTimeOut(true); 115 } 116 117 @Override 118 public void onDestroy() { 119 } 120 121 @Override 122 public IBinder onBind(Intent intent) { 123 // This service cannot be bound 124 return null; 125 } 126 127 /** 128 * Executes an explicit command. 129 * 130 * This is the entry point for arbitrary commands that are executed upon reception of certain 131 * events that should be executed on the context of this service. The supported commands are: 132 * - Check last update time and possibly schedule an update of the data for later. 133 * This is triggered every day, upon reception of the DATE_CHANGED_INTENT_ACTION broadcast. 134 * - Update data NOW. 135 * This is normally received upon trigger of the scheduled update. 136 * - Handle a finished download. 137 * This executes the actions that must be taken after a file (metadata or dictionary data 138 * has been downloaded (or failed to download). 139 * The commands that can be spun an another thread will be executed serially, in order, on 140 * a worker thread that is created on demand and terminates after a short while if there isn't 141 * any work left to do. 142 */ 143 @Override 144 public synchronized int onStartCommand(final Intent intent, final int flags, 145 final int startId) { 146 final DictionaryService self = this; 147 if (SHOW_DOWNLOAD_TOAST_INTENT_ACTION.equals(intent.getAction())) { 148 // This is a UI action, it can't be run in another thread 149 showStartDownloadingToast(this, LocaleUtils.constructLocaleFromString( 150 intent.getStringExtra(LOCALE_INTENT_ARGUMENT))); 151 } else { 152 // If it's a command that does not require UI, arrange for the work to be done on a 153 // separate thread, so that we can return right away. The executor will spawn a thread 154 // if necessary, or reuse a thread that has become idle as appropriate. 155 // DATE_CHANGED or UPDATE_NOW are examples of commands that can be done on another 156 // thread. 157 mExecutor.submit(new Runnable() { 158 @Override 159 public void run() { 160 dispatchBroadcast(self, intent); 161 // Since calls to onStartCommand are serialized, the submissions to the executor 162 // are serialized. That means we are guaranteed to call the stopSelfResult() 163 // in the same order that we got them, so we don't need to take care of the 164 // order. 165 stopSelfResult(startId); 166 } 167 }); 168 } 169 return Service.START_REDELIVER_INTENT; 170 } 171 172 private static void dispatchBroadcast(final Context context, final Intent intent) { 173 if (DATE_CHANGED_INTENT_ACTION.equals(intent.getAction())) { 174 // This happens when the date of the device changes. This normally happens 175 // at midnight local time, but it may happen if the user changes the date 176 // by hand or something similar happens. 177 checkTimeAndMaybeSetupUpdateAlarm(context); 178 } else if (DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION.equals(intent.getAction())) { 179 // Intent to trigger an update now. 180 UpdateHandler.tryUpdate(context, false); 181 } else { 182 UpdateHandler.downloadFinished(context, intent); 183 } 184 } 185 186 /** 187 * Setups an alarm to check for updates if an update is due. 188 */ 189 private static void checkTimeAndMaybeSetupUpdateAlarm(final Context context) { 190 // Of all clients, if the one that hasn't been updated for the longest 191 // is still more recent than UPDATE_FREQUENCY, do nothing. 192 if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY)) return; 193 194 PrivateLog.log("Date changed - registering alarm"); 195 AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); 196 197 // Best effort to wake between midnight and MAX_ALARM_DELAY in the morning. 198 // It doesn't matter too much if this is very inexact. 199 final long now = System.currentTimeMillis(); 200 final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY); 201 final Intent updateIntent = new Intent(DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION); 202 final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, 203 updateIntent, PendingIntent.FLAG_CANCEL_CURRENT); 204 205 // We set the alarm in the type that doesn't forcefully wake the device 206 // from sleep, but fires the next time the device actually wakes for any 207 // other reason. 208 if (null != alarmManager) alarmManager.set(AlarmManager.RTC, alarmTime, pendingIntent); 209 } 210 211 /** 212 * Utility method to decide whether the last update is older than a certain time. 213 * 214 * @return true if at least `time' milliseconds have elapsed since last update, false otherwise. 215 */ 216 private static boolean isLastUpdateAtLeastThisOld(final Context context, final long time) { 217 final long now = System.currentTimeMillis(); 218 final long lastUpdate = MetadataDbHelper.getOldestUpdateTime(context); 219 PrivateLog.log("Last update was " + lastUpdate); 220 return lastUpdate + time < now; 221 } 222 223 /** 224 * Refreshes data if it hasn't been refreshed in a very long time. 225 * 226 * This will check the last update time, and if it's been more than VERY_LONG_TIME, 227 * update metadata now - and possibly take subsequent update actions. 228 */ 229 public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) { 230 if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME)) return; 231 UpdateHandler.tryUpdate(context, false); 232 } 233 234 /** 235 * Shows a toast informing the user that an automatic dictionary download is starting. 236 */ 237 private static void showStartDownloadingToast(final Context context, final Locale locale) { 238 final String toastText = String.format( 239 context.getString(R.string.toast_downloading_suggestions), 240 locale.getDisplayName()); 241 Toast.makeText(context, toastText, Toast.LENGTH_LONG).show(); 242 } 243 } 244