/* * * * Copyright (C) 2015 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * / */ package org.anhonesteffort.flock; import android.accounts.Account; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.ContentProviderClient; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.Toast; import net.fortuna.ical4j.data.CalendarOutputter; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.ValidationException; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.property.Name; import net.fortuna.ical4j.model.property.Version; import org.anhonesteffort.flock.auth.DavAccount; import org.anhonesteffort.flock.sync.AbstractLocalComponentCollection; import org.anhonesteffort.flock.sync.InvalidLocalComponentException; import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; import org.anhonesteffort.flock.sync.addressbook.ContactFactory; import org.anhonesteffort.flock.sync.addressbook.LocalAddressbookStore; import org.anhonesteffort.flock.sync.addressbook.LocalContactCollection; import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; import org.anhonesteffort.flock.sync.calendar.LocalCalendarStore; import org.anhonesteffort.flock.sync.calendar.LocalEventCollection; import org.anhonesteffort.flock.util.guava.Optional; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedList; import java.util.List; import ezvcard.VCard; import ezvcard.VCardVersion; import ezvcard.io.text.VCardWriter; import ezvcard.property.Photo; import ezvcard.property.Uid; /** * Programmer: rhodey */ public class ExportService extends Service { private static final String TAG = ExportService.class.getSimpleName(); private static final int NOTIFY_ID = 1025; private ServiceHandler serviceHandler; private NotificationManager notifyManager; private NotificationCompat.Builder notificationBuilder; private int countFailedContactExports = 0; private int countFailedEventExports = 0; private enum EndState { SUCCESS, PROMPT_LOGIN, PROMPT_MAKE_SPACE, PROMPT_RESTART } private EndState endState = null; private void handleContactExportFailed() { countFailedContactExports++; Log.d(TAG, "contact export failed, counter: " + countFailedContactExports); } private void handleEventExportFailed() { countFailedEventExports++; Log.d(TAG, "event export failed, counter: " + countFailedEventExports); } private void handleInitializeNotification() { notificationBuilder .setProgress(0, 0, true) .setContentTitle(getString(R.string.export)) .setContentText(getString(R.string.exporting_contacts_and_calendars)) .setSmallIcon(R.drawable.flock_actionbar_icon); startForeground(NOTIFY_ID, notificationBuilder.build()); } private Optional<LocalContactCollection> getAddressbook(ContentProviderClient client, DavAccount account) { LocalAddressbookStore addressbookStore = new LocalAddressbookStore(getBaseContext(), client, account); List<LocalContactCollection> addressbooks = addressbookStore.getCollections(); if (addressbooks.isEmpty()) return Optional.absent(); else return Optional.of(addressbooks.get(0)); } private List<LocalEventCollection> getCalendars(ContentProviderClient client, Account account) throws RemoteException { LocalCalendarStore calendarStore = new LocalCalendarStore(client, account); return calendarStore.getCollections(); } private Optional<File> createExternalFile(String filename) { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { Log.w(TAG, "external media not mounted?"); return Optional.absent(); } try { File file = new File(Environment.getExternalStorageDirectory(), filename); if (file.exists()) return Optional.of(file); else if (file.createNewFile()) return Optional.of(file); return Optional.absent(); } catch (IOException e) { Log.w(TAG, "unable to create file " + filename, e); return Optional.absent(); } } private List<File> createFilesForCollections(List<AbstractLocalComponentCollection<?>> collections) { List<File> files = new LinkedList<>(); Optional<File> file = null; int calCount = 1; for (AbstractLocalComponentCollection collection : collections) { if (collection instanceof LocalContactCollection) file = createExternalFile(getString(R.string.flock_contacts_vcf)); else { file = createExternalFile(getString(R.string.flock_calendar_ical, calCount)); calCount++; } if (file.isPresent()) files.add(file.get()); } return files; } private void simulateExport(AbstractLocalComponentCollection<?> collection, File output) throws IOException, RemoteException { FileOutputStream stream = new FileOutputStream(output, false); try { for (int i = 0; i < collection.getComponentIds().size(); i++) stream.write(new byte[512]); } finally { stream.close(); } } private boolean isStorageSpaceAvailable(List<AbstractLocalComponentCollection<?>> collections, List<File> files) throws RemoteException { if (files.size() != collections.size()) { Log.w(TAG, "collection count and output file count differ"); return false; } try { for (int i = 0; i < collections.size(); i++) simulateExport(collections.get(i), files.get(i)); return true; } catch (IOException e) { Log.w(TAG, "error during export simulation, not enough space?", e); return false; } } private void handleExportContacts(LocalContactCollection addressbook, File output) throws RemoteException, IOException { VCardWriter vCardWriter = new VCardWriter(output, false, VCardVersion.V3_0); try { for (Long contactId : addressbook.getComponentIds()) { try { Optional<VCard> vCard = addressbook.getComponent(contactId); if (vCard.isPresent()) { vCard.get().removeProperties(Uid.class); vCard.get().removeProperties(Photo.class); vCard.get().removeExtendedProperty(ContactFactory.PROPERTY_STARRED); vCardWriter.write(vCard.get()); } else { Log.w(TAG, "couldn't find " + contactId + " in addressbook"); } } catch (InvalidLocalComponentException e) { handleContactExportFailed(); } } } finally { vCardWriter.close(); } } private void handleExportCalendars(List<LocalEventCollection> eventCollections, List<File> outputs) throws ValidationException, RemoteException, IOException { CalendarOutputter calendarWriter = new CalendarOutputter(false); for (int i = 0; i < eventCollections.size(); i++) { LocalEventCollection eventCollection = eventCollections.get(i); List<Long> eventIds = eventCollection.getComponentIds(); Calendar calendar = new Calendar(); FileOutputStream output = new FileOutputStream(outputs.get(i), false); Optional<String> displayName = eventCollection.getDisplayName(); if (displayName.isPresent() && !displayName.get().isEmpty()) calendar.getProperties().add(new Name(displayName.get())); try { for (Long eventId : eventIds) { try { Optional<Calendar> event = eventCollection.getComponent(eventId); if (event.isPresent()) { VEvent vEvent = (VEvent) event.get().getComponent(VEvent.VEVENT); if (vEvent != null) { if (vEvent.getProperty(Property.ORGANIZER) != null) vEvent.getProperties().remove(vEvent.getProperty(Property.ORGANIZER)); calendar.getComponents().add(vEvent); } else Log.w(TAG, "couldn't parse VEVENT from local calendar component"); } else { Log.w(TAG, "couldn't find " + eventId + " in calendar " + eventCollection.getPath()); } } catch (InvalidLocalComponentException e) { handleEventExportFailed(); } } calendar.getProperties().add(Version.VERSION_2_0); calendarWriter.output(calendar, output); } finally { output.close(); } } } private void handleIndexFilesWithMediaScanner(List<File> files) { for (File file : files) { sendBroadcast(new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file) )); } } private void handleExportComplete(EndState endState) { Log.d(TAG, "HANDLE EXPORT COMPLETE: " + endState); this.endState = endState; stopForeground(false); stopSelf(); } private void handlePromptLoginAndRetry() { Log.w(TAG, "HANDLE PROMPT LOGIN AND RETRY"); Intent clickIntent = new Intent(getBaseContext(), CorrectPasswordActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(getBaseContext(), 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT); notificationBuilder .setAutoCancel(true) .setProgress(0, 0, false) .setContentIntent(pendingIntent) .setContentTitle(getString(R.string.export_failed)) .setContentText(getString(R.string.tap_to_login_then_retry_export)); notifyManager.notify(NOTIFY_ID, notificationBuilder.build()); } private void handlePromptClearSpaceAndRetry() { Log.w(TAG, "HANDLE PROMPT CLEAR SPACE AND RETRY"); notificationBuilder .setProgress(0, 0, false) .setContentTitle(getString(R.string.export_failed)) .setContentText(getString(R.string.try_making_more_storage_space_available)); notifyManager.notify(NOTIFY_ID, notificationBuilder.build()); } private void handleUnrecoverableError() { Log.w(TAG, "HANDLE UNRECOVERABLE ERROR"); notificationBuilder .setProgress(0, 0, false) .setContentTitle(getString(R.string.export_failed)) .setContentText(getString(R.string.try_a_separate_export_app_if_error_continues)); notifyManager.notify(NOTIFY_ID, notificationBuilder.build()); } private void handleStartExport() { Log.d(TAG, "HANDLE START EXPORT"); handleInitializeNotification(); try { Optional<DavAccount> account = DavAccountHelper.getAccount(getBaseContext()); ContentProviderClient contactClient = getBaseContext().getContentResolver() .acquireContentProviderClient(AddressbookSyncScheduler.CONTENT_AUTHORITY); ContentProviderClient calendarClient = getBaseContext().getContentResolver() .acquireContentProviderClient(CalendarsSyncScheduler.CONTENT_AUTHORITY); if (account.isPresent()) { try { Optional<LocalContactCollection> addressbook = getAddressbook(contactClient, account.get()); List<LocalEventCollection> calendars = getCalendars(calendarClient, account.get().getOsAccount()); List<AbstractLocalComponentCollection<?>> collections = new LinkedList<>(); if (!addressbook.isPresent()) { throw new RemoteException("addressbook missing, what is going on?"); } collections.add(addressbook.get()); collections.addAll(calendars); List<File> outputFiles = createFilesForCollections(collections); if (isStorageSpaceAvailable(collections, outputFiles)) { File contactsFile = outputFiles.remove(0); handleExportContacts(addressbook.get(), contactsFile); handleExportCalendars(calendars, outputFiles); outputFiles.add(contactsFile); handleIndexFilesWithMediaScanner(outputFiles); handleExportComplete(EndState.SUCCESS); return; } else { handleExportComplete(EndState.PROMPT_MAKE_SPACE); return; } } catch (ValidationException e) { Log.e(TAG, "WTF ical4j", e); } catch (RemoteException e) { Log.e(TAG, "why android?", e); } catch (IOException e) { Log.e(TAG, "why android?", e); } } else { handleExportComplete(EndState.PROMPT_LOGIN); return; } } catch (Exception e) { Log.e(TAG, "caught unexpected runtime exception", e); } handleExportComplete(EndState.PROMPT_RESTART); } @Override public void onDestroy() { Log.d(TAG, "ON DESTROY"); switch (endState) { case PROMPT_LOGIN: handlePromptLoginAndRetry(); break; case PROMPT_MAKE_SPACE: handlePromptClearSpaceAndRetry(); break; case PROMPT_RESTART: handleUnrecoverableError(); break; } if (endState != EndState.SUCCESS) { Toast.makeText(getBaseContext(), R.string.export_failed, Toast.LENGTH_SHORT).show(); return; } if (countFailedContactExports == 0 && countFailedEventExports == 0) { notificationBuilder .setProgress(0, 0, false) .setContentTitle(getString(R.string.export_complete)) .setContentText(getString(R.string.export_completed_successfully)); } else { notificationBuilder .setProgress(0, 0, false) .setContentTitle(getString(R.string.export_complete)) .setContentText(getString( R.string.failed_to_copy_contacts_and_events, countFailedContactExports, countFailedEventExports )); } notifyManager.notify(NOTIFY_ID, notificationBuilder.build()); Toast.makeText(getBaseContext(), R.string.export_complete, Toast.LENGTH_SHORT).show(); } @Override public void onCreate() { HandlerThread thread = new HandlerThread(getClass().getSimpleName(), HandlerThread.NORM_PRIORITY); thread.start(); serviceHandler = new ServiceHandler(thread.getLooper()); notifyManager = (NotificationManager)getBaseContext().getSystemService(Context.NOTIFICATION_SERVICE); notificationBuilder = new NotificationCompat.Builder(getBaseContext()); } private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { handleStartExport(); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { serviceHandler.sendMessage(serviceHandler.obtainMessage()); return START_STICKY; } @Override public IBinder onBind(Intent intent) { return null; } }