Home | History | Annotate | Download | only in dictionarypack
      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
    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
    135     public void onDestroy() {
    136     }
    137 
    138     @Override
    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
    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 
    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      */
    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      */
    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      */
    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      */
    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