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