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