• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.text.format.DateUtils;
26 import android.util.Log;
27 import android.widget.Toast;
28 
29 import com.android.inputmethod.latin.R;
30 
31 import java.util.Locale;
32 import java.util.Random;
33 
34 /**
35  * Service that handles background tasks for the dictionary provider.
36  *
37  * This service provides the context for the long-running operations done by the
38  * dictionary provider. Those include:
39  * - Checking for the last update date and scheduling the next update. This runs every
40  *   day around midnight, upon reception of the DATE_CHANGED_INTENT_ACTION broadcast.
41  *   Every four days, it schedules an update of the metadata with the alarm manager.
42  * - Issuing the order to update the metadata. This runs every four days, between 0 and
43  *   6, upon reception of the UPDATE_NOW_INTENT_ACTION broadcast sent by the alarm manager
44  *   as a result of the above action.
45  * - Handling a download that just ended. These come in two flavors:
46  *   - Metadata is finished downloading. We should check whether there are new dictionaries
47  *     available, and download those that we need that have new versions.
48  *   - A dictionary file finished downloading. We should put the file ready for a client IME
49  *     to access, and mark the current state as such.
50  */
51 public final class DictionaryService extends Service {
52     private static final String TAG = DictionaryService.class.getName();
53 
54     /**
55      * The package name, to use in the intent actions.
56      */
57     private static final String PACKAGE_NAME = "com.android.android.inputmethod.latin";
58 
59     /**
60      * The action of the intent to tell the dictionary provider to update now.
61      */
62     private static final String UPDATE_NOW_INTENT_ACTION = PACKAGE_NAME + ".UPDATE_NOW";
63 
64     /**
65      * The action of the date changing, used to schedule a periodic freshness check
66      */
67     private static final String DATE_CHANGED_INTENT_ACTION =
68             Intent.ACTION_DATE_CHANGED;
69 
70     /**
71      * The action of displaying a toast to warn the user an automatic download is starting.
72      */
73     /* package */ static final String SHOW_DOWNLOAD_TOAST_INTENT_ACTION =
74             PACKAGE_NAME + ".SHOW_DOWNLOAD_TOAST_INTENT_ACTION";
75 
76     /**
77      * A locale argument, as a String.
78      */
79     /* package */ static final String LOCALE_INTENT_ARGUMENT = "locale";
80 
81     /**
82      * How often, in milliseconds, we want to update the metadata. This is a
83      * floor value; actually, it may happen several hours later, or even more.
84      */
85     private static final long UPDATE_FREQUENCY = 4 * DateUtils.DAY_IN_MILLIS;
86 
87     /**
88      * We are waked around midnight, local time. We want to wake between midnight and 6 am,
89      * roughly. So use a random time between 0 and this delay.
90      */
91     private static final int MAX_ALARM_DELAY = 6 * ((int)AlarmManager.INTERVAL_HOUR);
92 
93     /**
94      * How long we consider a "very long time". If no update took place in this time,
95      * the content provider will trigger an update in the background.
96      */
97     private static final long VERY_LONG_TIME = 14 * DateUtils.DAY_IN_MILLIS;
98 
99     /**
100      * The last seen start Id. This must be stored because we must only call stopSelfResult() with
101      * the last seen Id, or the service won't stop.
102      */
103     private int mLastSeenStartId;
104 
105     /**
106      * The command count. We need this because we need to not call stopSelfResult() while we still
107      * have commands running.
108      */
109     private int mCommandCount;
110 
111     @Override
onCreate()112     public void onCreate() {
113         mLastSeenStartId = 0;
114         mCommandCount = 0;
115     }
116 
117     @Override
onDestroy()118     public void onDestroy() {
119     }
120 
121     @Override
onBind(Intent intent)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      */
140     @Override
onStartCommand(final Intent intent, final int flags, final int startId)141     public synchronized int onStartCommand(final Intent intent, final int flags,
142             final int startId) {
143         final DictionaryService self = this;
144         mLastSeenStartId = startId;
145         mCommandCount += 1;
146         if (SHOW_DOWNLOAD_TOAST_INTENT_ACTION.equals(intent.getAction())) {
147             // This is a UI action, it can't be run in another thread
148             showStartDownloadingToast(this, LocaleUtils.constructLocaleFromString(
149                     intent.getStringExtra(LOCALE_INTENT_ARGUMENT)));
150         } else {
151             // If it's a command that does not require UI, create a thread to do the work
152             // and return right away. DATE_CHANGED or UPDATE_NOW are examples of such commands.
153             new Thread("updateOrFinishDownload") {
154                 @Override
155                 public void run() {
156                     dispatchBroadcast(self, intent);
157                     synchronized(self) {
158                         if (--mCommandCount <= 0) {
159                             if (!stopSelfResult(mLastSeenStartId)) {
160                                 Log.e(TAG, "Can't stop ourselves");
161                             }
162                         }
163                     }
164                 }
165             }.start();
166         }
167         return Service.START_REDELIVER_INTENT;
168     }
169 
dispatchBroadcast(final Context context, final Intent intent)170     private static void dispatchBroadcast(final Context context, final Intent intent) {
171         if (DATE_CHANGED_INTENT_ACTION.equals(intent.getAction())) {
172             // This happens when the date of the device changes. This normally happens
173             // at midnight local time, but it may happen if the user changes the date
174             // by hand or something similar happens.
175             checkTimeAndMaybeSetupUpdateAlarm(context);
176         } else if (UPDATE_NOW_INTENT_ACTION.equals(intent.getAction())) {
177             // Intent to trigger an update now.
178             UpdateHandler.update(context, false);
179         } else {
180             UpdateHandler.downloadFinished(context, intent);
181         }
182     }
183 
184     /**
185      * Setups an alarm to check for updates if an update is due.
186      */
checkTimeAndMaybeSetupUpdateAlarm(final Context context)187     private static void checkTimeAndMaybeSetupUpdateAlarm(final Context context) {
188         // Of all clients, if the one that hasn't been updated for the longest
189         // is still more recent than UPDATE_FREQUENCY, do nothing.
190         if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY)) return;
191 
192         PrivateLog.log("Date changed - registering alarm");
193         AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
194 
195         // Best effort to wake between midnight and MAX_ALARM_DELAY in the morning.
196         // It doesn't matter too much if this is very inexact.
197         final long now = System.currentTimeMillis();
198         final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY);
199         final Intent updateIntent = new Intent(DictionaryService.UPDATE_NOW_INTENT_ACTION);
200         final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
201                 updateIntent, PendingIntent.FLAG_CANCEL_CURRENT);
202 
203         // We set the alarm in the type that doesn't forcefully wake the device
204         // from sleep, but fires the next time the device actually wakes for any
205         // other reason.
206         if (null != alarmManager) alarmManager.set(AlarmManager.RTC, alarmTime, pendingIntent);
207     }
208 
209     /**
210      * Utility method to decide whether the last update is older than a certain time.
211      *
212      * @return true if at least `time' milliseconds have elapsed since last update, false otherwise.
213      */
isLastUpdateAtLeastThisOld(final Context context, final long time)214     private static boolean isLastUpdateAtLeastThisOld(final Context context, final long time) {
215         final long now = System.currentTimeMillis();
216         final long lastUpdate = MetadataDbHelper.getOldestUpdateTime(context);
217         PrivateLog.log("Last update was " + lastUpdate);
218         return lastUpdate + time < now;
219     }
220 
221     /**
222      * Refreshes data if it hasn't been refreshed in a very long time.
223      *
224      * This will check the last update time, and if it's been more than VERY_LONG_TIME,
225      * update metadata now - and possibly take subsequent update actions.
226      */
updateNowIfNotUpdatedInAVeryLongTime(final Context context)227     public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) {
228         if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME)) return;
229         UpdateHandler.update(context, false);
230     }
231 
232     /**
233      * Shows a toast informing the user that an automatic dictionary download is starting.
234      */
showStartDownloadingToast(final Context context, final Locale locale)235     private static void showStartDownloadingToast(final Context context, final Locale locale) {
236         final String toastText = String.format(
237                 context.getString(R.string.toast_downloading_suggestions),
238                 locale.getDisplayName());
239         Toast.makeText(context, toastText, Toast.LENGTH_LONG).show();
240     }
241 }
242