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