blob: f21e1e1af17a412a5f373fc8a5014e577ad57ab4 [file] [log] [blame]
Marcus Hagerott95246bb2016-11-11 10:56:09 -08001/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.contacts;
17
18import android.app.Notification;
19import android.app.NotificationManager;
20import android.app.PendingIntent;
21import android.app.Service;
22import android.content.Context;
23import android.content.Intent;
24import android.content.OperationApplicationException;
25import android.os.AsyncTask;
26import android.os.IBinder;
27import android.os.RemoteException;
28import android.support.annotation.Nullable;
29import android.support.v4.app.NotificationCompat;
30import android.support.v4.content.LocalBroadcastManager;
31import android.util.TimingLogger;
32
33import com.android.contacts.activities.PeopleActivity;
Gary Mai69c182a2016-12-05 13:07:03 -080034import com.android.contacts.database.SimContactDao;
35import com.android.contacts.model.SimCard;
36import com.android.contacts.model.SimContact;
37import com.android.contacts.model.account.AccountWithDataSet;
Marcus Hagerott95246bb2016-11-11 10:56:09 -080038import com.android.contactsbind.FeedbackHelper;
39
40import java.util.ArrayList;
41import java.util.List;
42import java.util.concurrent.ExecutorService;
43import java.util.concurrent.Executors;
44
45/**
46 * Imports {@link SimContact}s from a background thread
47 */
48public class SimImportService extends Service {
49
50 private static final String TAG = "SimImportService";
51
Marcus Hagerott2bb49842016-11-15 18:26:20 -080052 /**
53 * Wrapper around the service state for testability
54 */
55 public interface StatusProvider {
56
57 /**
58 * Returns whether there is any imports still pending
59 *
60 * <p>This should be called from the UI thread</p>
61 */
62 boolean isRunning();
63
64 /**
65 * Returns whether an import for sim has been requested
66 *
67 * <p>This should be called from the UI thread</p>
68 */
69 boolean isImporting(SimCard sim);
70 }
71
Marcus Hagerott95246bb2016-11-11 10:56:09 -080072 public static final String EXTRA_ACCOUNT = "account";
73 public static final String EXTRA_SIM_CONTACTS = "simContacts";
74 public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
75 public static final String EXTRA_RESULT_CODE = "resultCode";
76 public static final String EXTRA_RESULT_COUNT = "count";
77 public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime";
78
79 public static final String BROADCAST_SERVICE_STATE_CHANGED =
80 SimImportService.class.getName() + "#serviceStateChanged";
81 public static final String BROADCAST_SIM_IMPORT_COMPLETE =
82 SimImportService.class.getName() + "#simImportComplete";
83
84 public static final int RESULT_UNKNOWN = 0;
85 public static final int RESULT_SUCCESS = 1;
86 public static final int RESULT_FAILURE = 2;
87
88 // VCardService uses jobIds for it's notifications which count up from 0 so we just use a
89 // bigger number to prevent overlap.
90 private static final int NOTIFICATION_ID = 100;
91
92 private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
93
94 // Keeps track of current tasks. This is only modified from the UI thread.
95 private static List<ImportTask> sPending = new ArrayList<>();
96
Marcus Hagerott2bb49842016-11-15 18:26:20 -080097 private static StatusProvider sStatusProvider = new StatusProvider() {
98 @Override
99 public boolean isRunning() {
100 return !sPending.isEmpty();
101 }
102
103 @Override
104 public boolean isImporting(SimCard sim) {
105 return SimImportService.isImporting(sim);
106 }
107 };
108
Marcus Hagerott95246bb2016-11-11 10:56:09 -0800109 /**
110 * Returns whether an import for sim has been requested
111 *
112 * <p>This should be called from the UI thread</p>
113 */
Marcus Hagerott2bb49842016-11-15 18:26:20 -0800114 private static boolean isImporting(SimCard sim) {
Marcus Hagerott95246bb2016-11-11 10:56:09 -0800115 for (ImportTask task : sPending) {
116 if (task.getSim().equals(sim)) {
117 return true;
118 }
119 }
120 return false;
121 }
122
Marcus Hagerott2bb49842016-11-15 18:26:20 -0800123 public static StatusProvider getStatusProvider() {
124 return sStatusProvider;
Marcus Hagerott95246bb2016-11-11 10:56:09 -0800125 }
126
127 /**
128 * Starts an import of the contacts from the sim into the target account
129 *
130 * @param context context to use for starting the service
131 * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
132 * {@link android.telephony.SubscriptionInfo#getSubscriptionId()}.
133 * Upon completion the SIM for that subscription ID will be marked as
134 * imported
135 * @param contacts the contacts to import
136 * @param targetAccount the account import the contacts into
137 */
138 public static void startImport(Context context, int subscriptionId,
139 ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
140 context.startService(new Intent(context, SimImportService.class)
141 .putExtra(EXTRA_SIM_CONTACTS, contacts)
142 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
143 .putExtra(EXTRA_ACCOUNT, targetAccount));
144 }
145
146
147 @Nullable
148 @Override
149 public IBinder onBind(Intent intent) {
150 return null;
151 }
152
153 @Override
154 public int onStartCommand(Intent intent, int flags, final int startId) {
155 final ImportTask task = createTaskForIntent(intent, startId);
156 if (task == null) {
157 new StopTask(this, startId).executeOnExecutor(mExecutor);
158 return START_NOT_STICKY;
159 }
160 sPending.add(task);
161 task.executeOnExecutor(mExecutor);
162 notifyStateChanged();
163 return START_REDELIVER_INTENT;
164 }
165
166 @Override
167 public void onDestroy() {
168 super.onDestroy();
169 mExecutor.shutdown();
170 }
171
172 private ImportTask createTaskForIntent(Intent intent, int startId) {
173 final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
174 final ArrayList<SimContact> contacts =
175 intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
176
177 final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
178 SimCard.NO_SUBSCRIPTION_ID);
179 final SimContactDao dao = SimContactDao.create(this);
180 final SimCard sim = dao.getSimBySubscriptionId(subscriptionId);
181 if (sim != null) {
182 return new ImportTask(sim, contacts, targetAccount, dao, startId);
183 } else {
184 return null;
185 }
186 }
187
188 private Notification getCompletedNotification() {
189 final Intent intent = new Intent(this, PeopleActivity.class);
190 final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
191 builder.setOngoing(false)
192 .setAutoCancel(true)
193 .setContentTitle(this.getString(R.string.importing_sim_finished_title))
194 .setColor(this.getResources().getColor(R.color.dialtacts_theme_color))
John Shaobd9ef3c2016-12-15 12:42:03 -0800195 .setSmallIcon(R.drawable.quantum_ic_done_vd_theme_24)
Marcus Hagerott95246bb2016-11-11 10:56:09 -0800196 .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0));
197 return builder.build();
198 }
199
200 private Notification getFailedNotification() {
201 final Intent intent = new Intent(this, PeopleActivity.class);
202 final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
203 builder.setOngoing(false)
204 .setAutoCancel(true)
205 .setContentTitle(this.getString(R.string.importing_sim_failed_title))
Marcus Hagerott4dc12242016-12-06 14:29:49 -0800206 .setContentText(this.getString(R.string.importing_sim_failed_message))
Marcus Hagerott95246bb2016-11-11 10:56:09 -0800207 .setColor(this.getResources().getColor(R.color.dialtacts_theme_color))
John Shaobd9ef3c2016-12-15 12:42:03 -0800208 .setSmallIcon(R.drawable.quantum_ic_error_vd_theme_24)
Marcus Hagerott95246bb2016-11-11 10:56:09 -0800209 .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0));
210 return builder.build();
211 }
212
213 private Notification getImportingNotification() {
214 final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
215 final String description = getString(R.string.importing_sim_in_progress_title);
216 builder.setOngoing(true)
217 .setProgress(/* current */ 0, /* max */ 100, /* indeterminate */ true)
218 .setContentTitle(description)
219 .setColor(this.getResources().getColor(R.color.dialtacts_theme_color))
220 .setSmallIcon(android.R.drawable.stat_sys_download);
221 return builder.build();
222 }
223
224 private void notifyStateChanged() {
225 LocalBroadcastManager.getInstance(this).sendBroadcast(
226 new Intent(BROADCAST_SERVICE_STATE_CHANGED));
227 }
228
229 // Schedule a task that calls stopSelf when it completes. This is used to ensure that the
230 // calls to stopSelf occur in the correct order (because this service uses a single thread
231 // executor this won't run until all work that was requested before it has finished)
232 private static class StopTask extends AsyncTask<Void, Void, Void> {
233 private Service mHost;
234 private final int mStartId;
235
236 private StopTask(Service host, int startId) {
237 mHost = host;
238 mStartId = startId;
239 }
240
241 @Override
242 protected Void doInBackground(Void... params) {
243 return null;
244 }
245
246 @Override
247 protected void onPostExecute(Void aVoid) {
248 super.onPostExecute(aVoid);
249 mHost.stopSelf(mStartId);
250 }
251 }
252
253 private class ImportTask extends AsyncTask<Void, Void, Boolean> {
254 private final SimCard mSim;
255 private final List<SimContact> mContacts;
256 private final AccountWithDataSet mTargetAccount;
257 private final SimContactDao mDao;
258 private final NotificationManager mNotificationManager;
259 private final int mStartId;
260 private final long mStartTime;
261
262 public ImportTask(SimCard sim, List<SimContact> contacts, AccountWithDataSet targetAccount,
263 SimContactDao dao, int startId) {
264 mSim = sim;
265 mContacts = contacts;
266 mTargetAccount = targetAccount;
267 mDao = dao;
268 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
269 mStartId = startId;
270 mStartTime = System.currentTimeMillis();
271 }
272
273 @Override
274 protected void onPreExecute() {
275 super.onPreExecute();
276 startForeground(NOTIFICATION_ID, getImportingNotification());
277 }
278
279 @Override
280 protected Boolean doInBackground(Void... params) {
281 final TimingLogger timer = new TimingLogger(TAG, "import");
282 try {
283 // Just import them all at once.
284 // Experimented with using smaller batches (e.g. 25 and 50) so that percentage
285 // progress could be displayed however this slowed down the import by over a factor
286 // of 2. If the batch size is over a 100 then most cases will only require a single
287 // batch so we don't even worry about displaying accurate progress
288 mDao.importContacts(mContacts, mTargetAccount);
289 mDao.persistSimState(mSim.withImportedState(true));
290 timer.addSplit("done");
291 timer.dumpToLog();
292 } catch (RemoteException|OperationApplicationException e) {
293 FeedbackHelper.sendFeedback(SimImportService.this, TAG,
294 "Failed to import contacts from SIM card", e);
295 return false;
296 }
297 return true;
298 }
299
300 public SimCard getSim() {
301 return mSim;
302 }
303
304 @Override
305 protected void onPostExecute(Boolean success) {
306 super.onPostExecute(success);
307 stopSelf(mStartId);
308
309 Intent result;
310 final Notification notification;
311 if (success) {
312 result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
313 .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
314 .putExtra(EXTRA_RESULT_COUNT, mContacts.size())
315 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime)
316 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId());
317
318 notification = getCompletedNotification();
319 } else {
320 result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
321 .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
322 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime)
323 .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId());
324
325 notification = getFailedNotification();
326 }
327 LocalBroadcastManager.getInstance(SimImportService.this).sendBroadcast(result);
328
329 sPending.remove(this);
330
331 // Only notify of completion if all the import requests have finished. We're using
332 // the same notification for imports so in the rare case that a user has started
333 // multiple imports the notification won't go away until all of them complete.
334 if (sPending.isEmpty()) {
335 stopForeground(false);
336 mNotificationManager.notify(NOTIFICATION_ID, notification);
337 }
338 notifyStateChanged();
339 }
340 }
341}