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