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