Marcus Hagerott | 95246bb | 2016-11-11 10:56:09 -0800 | [diff] [blame] | 1 | /* |
| 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 | */ |
| 16 | package com.android.contacts; |
| 17 | |
| 18 | import android.app.Notification; |
| 19 | import android.app.NotificationManager; |
| 20 | import android.app.PendingIntent; |
| 21 | import android.app.Service; |
| 22 | import android.content.Context; |
| 23 | import android.content.Intent; |
| 24 | import android.content.OperationApplicationException; |
| 25 | import android.os.AsyncTask; |
| 26 | import android.os.IBinder; |
| 27 | import android.os.RemoteException; |
| 28 | import android.support.annotation.Nullable; |
| 29 | import android.support.v4.app.NotificationCompat; |
| 30 | import android.support.v4.content.LocalBroadcastManager; |
| 31 | import android.util.TimingLogger; |
| 32 | |
| 33 | import com.android.contacts.activities.PeopleActivity; |
Gary Mai | 69c182a | 2016-12-05 13:07:03 -0800 | [diff] [blame] | 34 | import com.android.contacts.database.SimContactDao; |
| 35 | import com.android.contacts.model.SimCard; |
| 36 | import com.android.contacts.model.SimContact; |
| 37 | import com.android.contacts.model.account.AccountWithDataSet; |
Marcus Hagerott | 95246bb | 2016-11-11 10:56:09 -0800 | [diff] [blame] | 38 | import com.android.contactsbind.FeedbackHelper; |
| 39 | |
| 40 | import java.util.ArrayList; |
| 41 | import java.util.List; |
| 42 | import java.util.concurrent.ExecutorService; |
| 43 | import java.util.concurrent.Executors; |
| 44 | |
| 45 | /** |
| 46 | * Imports {@link SimContact}s from a background thread |
| 47 | */ |
| 48 | public class SimImportService extends Service { |
| 49 | |
| 50 | private static final String TAG = "SimImportService"; |
| 51 | |
Marcus Hagerott | 2bb4984 | 2016-11-15 18:26:20 -0800 | [diff] [blame] | 52 | /** |
| 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 Hagerott | 95246bb | 2016-11-11 10:56:09 -0800 | [diff] [blame] | 72 | 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 Hagerott | 2bb4984 | 2016-11-15 18:26:20 -0800 | [diff] [blame] | 97 | 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 Hagerott | 95246bb | 2016-11-11 10:56:09 -0800 | [diff] [blame] | 109 | /** |
| 110 | * Returns whether an import for sim has been requested |
| 111 | * |
| 112 | * <p>This should be called from the UI thread</p> |
| 113 | */ |
Marcus Hagerott | 2bb4984 | 2016-11-15 18:26:20 -0800 | [diff] [blame] | 114 | private static boolean isImporting(SimCard sim) { |
Marcus Hagerott | 95246bb | 2016-11-11 10:56:09 -0800 | [diff] [blame] | 115 | for (ImportTask task : sPending) { |
| 116 | if (task.getSim().equals(sim)) { |
| 117 | return true; |
| 118 | } |
| 119 | } |
| 120 | return false; |
| 121 | } |
| 122 | |
Marcus Hagerott | 2bb4984 | 2016-11-15 18:26:20 -0800 | [diff] [blame] | 123 | public static StatusProvider getStatusProvider() { |
| 124 | return sStatusProvider; |
Marcus Hagerott | 95246bb | 2016-11-11 10:56:09 -0800 | [diff] [blame] | 125 | } |
| 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 Shao | bd9ef3c | 2016-12-15 12:42:03 -0800 | [diff] [blame] | 195 | .setSmallIcon(R.drawable.quantum_ic_done_vd_theme_24) |
Marcus Hagerott | 95246bb | 2016-11-11 10:56:09 -0800 | [diff] [blame] | 196 | .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 Hagerott | 4dc1224 | 2016-12-06 14:29:49 -0800 | [diff] [blame] | 206 | .setContentText(this.getString(R.string.importing_sim_failed_message)) |
Marcus Hagerott | 95246bb | 2016-11-11 10:56:09 -0800 | [diff] [blame] | 207 | .setColor(this.getResources().getColor(R.color.dialtacts_theme_color)) |
John Shao | bd9ef3c | 2016-12-15 12:42:03 -0800 | [diff] [blame] | 208 | .setSmallIcon(R.drawable.quantum_ic_error_vd_theme_24) |
Marcus Hagerott | 95246bb | 2016-11-11 10:56:09 -0800 | [diff] [blame] | 209 | .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 | } |