/*
* 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();
}
}
}