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
17package com.android.inputmethod.dictionarypack;
18
19import android.app.AlarmManager;
20import android.app.PendingIntent;
21import android.app.Service;
22import android.content.Context;
23import android.content.Intent;
24import android.os.IBinder;
25import android.widget.Toast;
26
27import com.android.inputmethod.latin.R;
28
29import java.util.Locale;
30import java.util.Random;
31import java.util.concurrent.LinkedBlockingQueue;
32import java.util.concurrent.ThreadPoolExecutor;
33import 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 */
52public 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