Home | History | Annotate | Download | only in contacts
      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;
     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          */
     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          */
     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      */
    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 
    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      */
    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
    150     public IBinder onBind(Intent intent) {
    151         return null;
    152     }
    153 
    154     @Override
    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
    169     public void onDestroy() {
    170         super.onDestroy();
    171         mExecutor.shutdown();
    172     }
    173 
    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 
    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 
    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 
    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 
    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 
    241         private StopTask(Service host, int startId) {
    242             mHost = host;
    243             mStartId = startId;
    244         }
    245 
    246         @Override
    247         protected Void doInBackground(Void... params) {
    248             return null;
    249         }
    250 
    251         @Override
    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 
    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
    279         protected void onPreExecute() {
    280             super.onPreExecute();
    281             startForeground(NOTIFICATION_ID, getImportingNotification());
    282         }
    283 
    284         @Override
    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 
    305         public SimCard getSim() {
    306             return mSim;
    307         }
    308 
    309         @Override
    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