/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.contacts; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.OperationApplicationException; import android.os.AsyncTask; import android.os.IBinder; import android.os.RemoteException; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.TimingLogger; import com.android.contacts.activities.PeopleActivity; import com.android.contacts.database.SimContactDao; import com.android.contacts.model.SimCard; import com.android.contacts.model.SimContact; import com.android.contacts.model.account.AccountWithDataSet; import com.android.contacts.util.ContactsNotificationChannelsUtil; import com.android.contactsbind.FeedbackHelper; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Imports {@link SimContact}s from a background thread */ public class SimImportService extends Service { private static final String TAG = "SimImportService"; /** * Wrapper around the service state for testability */ public interface StatusProvider { /** * Returns whether there is any imports still pending * *

This should be called from the UI thread

*/ boolean isRunning(); /** * Returns whether an import for sim has been requested * *

This should be called from the UI thread

*/ boolean isImporting(SimCard sim); } public static final String EXTRA_ACCOUNT = "account"; public static final String EXTRA_SIM_CONTACTS = "simContacts"; public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId"; public static final String EXTRA_RESULT_CODE = "resultCode"; public static final String EXTRA_RESULT_COUNT = "count"; public static final String EXTRA_OPERATION_REQUESTED_AT_TIME = "requestedTime"; public static final String BROADCAST_SERVICE_STATE_CHANGED = SimImportService.class.getName() + "#serviceStateChanged"; public static final String BROADCAST_SIM_IMPORT_COMPLETE = SimImportService.class.getName() + "#simImportComplete"; public static final int RESULT_UNKNOWN = 0; public static final int RESULT_SUCCESS = 1; public static final int RESULT_FAILURE = 2; // VCardService uses jobIds for it's notifications which count up from 0 so we just use a // bigger number to prevent overlap. private static final int NOTIFICATION_ID = 100; private ExecutorService mExecutor = Executors.newSingleThreadExecutor(); // Keeps track of current tasks. This is only modified from the UI thread. private static List sPending = new ArrayList<>(); private static StatusProvider sStatusProvider = new StatusProvider() { @Override public boolean isRunning() { return !sPending.isEmpty(); } @Override public boolean isImporting(SimCard sim) { return SimImportService.isImporting(sim); } }; /** * Returns whether an import for sim has been requested * *

This should be called from the UI thread

*/ private static boolean isImporting(SimCard sim) { for (ImportTask task : sPending) { if (task.getSim().equals(sim)) { return true; } } return false; } public static StatusProvider getStatusProvider() { return sStatusProvider; } /** * Starts an import of the contacts from the sim into the target account * * @param context context to use for starting the service * @param subscriptionId the subscriptionId of the SIM card that is being imported. See * {@link android.telephony.SubscriptionInfo#getSubscriptionId()}. * Upon completion the SIM for that subscription ID will be marked as * imported * @param contacts the contacts to import * @param targetAccount the account import the contacts into */ public static void startImport(Context context, int subscriptionId, ArrayList contacts, AccountWithDataSet targetAccount) { context.startService(new Intent(context, SimImportService.class) .putExtra(EXTRA_SIM_CONTACTS, contacts) .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId) .putExtra(EXTRA_ACCOUNT, targetAccount)); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, final int startId) { ContactsNotificationChannelsUtil.createDefaultChannel(this); final ImportTask task = createTaskForIntent(intent, startId); if (task == null) { new StopTask(this, startId).executeOnExecutor(mExecutor); return START_NOT_STICKY; } sPending.add(task); task.executeOnExecutor(mExecutor); notifyStateChanged(); return START_REDELIVER_INTENT; } @Override public void onDestroy() { super.onDestroy(); mExecutor.shutdown(); } private ImportTask createTaskForIntent(Intent intent, int startId) { final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT); final ArrayList contacts = intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS); final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID, SimCard.NO_SUBSCRIPTION_ID); final SimContactDao dao = SimContactDao.create(this); final SimCard sim = dao.getSimBySubscriptionId(subscriptionId); if (sim != null) { return new ImportTask(sim, contacts, targetAccount, dao, startId); } else { return null; } } private Notification getCompletedNotification() { final Intent intent = new Intent(this, PeopleActivity.class); final NotificationCompat.Builder builder = new NotificationCompat.Builder( this, ContactsNotificationChannelsUtil.DEFAULT_CHANNEL); builder.setOngoing(false) .setAutoCancel(true) .setContentTitle(this.getString(R.string.importing_sim_finished_title)) .setColor(this.getResources().getColor(R.color.dialtacts_theme_color)) .setSmallIcon(R.drawable.quantum_ic_done_vd_theme_24) .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0)); return builder.build(); } private Notification getFailedNotification() { final Intent intent = new Intent(this, PeopleActivity.class); final NotificationCompat.Builder builder = new NotificationCompat.Builder( this, ContactsNotificationChannelsUtil.DEFAULT_CHANNEL); builder.setOngoing(false) .setAutoCancel(true) .setContentTitle(this.getString(R.string.importing_sim_failed_title)) .setContentText(this.getString(R.string.importing_sim_failed_message)) .setColor(this.getResources().getColor(R.color.dialtacts_theme_color)) .setSmallIcon(R.drawable.quantum_ic_error_vd_theme_24) .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0)); return builder.build(); } private Notification getImportingNotification() { final NotificationCompat.Builder builder = new NotificationCompat.Builder( this, ContactsNotificationChannelsUtil.DEFAULT_CHANNEL); final String description = getString(R.string.importing_sim_in_progress_title); builder.setOngoing(true) .setProgress(/* current */ 0, /* max */ 100, /* indeterminate */ true) .setContentTitle(description) .setColor(this.getResources().getColor(R.color.dialtacts_theme_color)) .setSmallIcon(android.R.drawable.stat_sys_download); return builder.build(); } private void notifyStateChanged() { LocalBroadcastManager.getInstance(this).sendBroadcast( new Intent(BROADCAST_SERVICE_STATE_CHANGED)); } // Schedule a task that calls stopSelf when it completes. This is used to ensure that the // calls to stopSelf occur in the correct order (because this service uses a single thread // executor this won't run until all work that was requested before it has finished) private static class StopTask extends AsyncTask { private Service mHost; private final int mStartId; private StopTask(Service host, int startId) { mHost = host; mStartId = startId; } @Override protected Void doInBackground(Void... params) { return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); mHost.stopSelf(mStartId); } } private class ImportTask extends AsyncTask { private final SimCard mSim; private final List mContacts; private final AccountWithDataSet mTargetAccount; private final SimContactDao mDao; private final NotificationManager mNotificationManager; private final int mStartId; private final long mStartTime; public ImportTask(SimCard sim, List contacts, AccountWithDataSet targetAccount, SimContactDao dao, int startId) { mSim = sim; mContacts = contacts; mTargetAccount = targetAccount; mDao = dao; mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); mStartId = startId; mStartTime = System.currentTimeMillis(); } @Override protected void onPreExecute() { super.onPreExecute(); startForeground(NOTIFICATION_ID, getImportingNotification()); } @Override protected Boolean doInBackground(Void... params) { final TimingLogger timer = new TimingLogger(TAG, "import"); try { // Just import them all at once. // Experimented with using smaller batches (e.g. 25 and 50) so that percentage // progress could be displayed however this slowed down the import by over a factor // of 2. If the batch size is over a 100 then most cases will only require a single // batch so we don't even worry about displaying accurate progress mDao.importContacts(mContacts, mTargetAccount); mDao.persistSimState(mSim.withImportedState(true)); timer.addSplit("done"); timer.dumpToLog(); } catch (RemoteException|OperationApplicationException e) { FeedbackHelper.sendFeedback(SimImportService.this, TAG, "Failed to import contacts from SIM card", e); return false; } return true; } public SimCard getSim() { return mSim; } @Override protected void onPostExecute(Boolean success) { super.onPostExecute(success); stopSelf(mStartId); Intent result; final Notification notification; if (success) { result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE) .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS) .putExtra(EXTRA_RESULT_COUNT, mContacts.size()) .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime) .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId()); notification = getCompletedNotification(); } else { result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE) .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE) .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, mStartTime) .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, mSim.getSubscriptionId()); notification = getFailedNotification(); } LocalBroadcastManager.getInstance(SimImportService.this).sendBroadcast(result); sPending.remove(this); // Only notify of completion if all the import requests have finished. We're using // the same notification for imports so in the rare case that a user has started // multiple imports the notification won't go away until all of them complete. if (sPending.isEmpty()) { stopForeground(false); mNotificationManager.notify(NOTIFICATION_ID, notification); } notifyStateChanged(); } } }