package com.orgzly.android.ui.fragments; import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.res.Resources; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.support.v4.app.Fragment; import android.support.v4.content.LocalBroadcastManager; import android.text.format.DateUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ProgressBar; import android.widget.TextView; import com.orgzly.BuildConfig; import com.orgzly.R; import com.orgzly.android.Book; import com.orgzly.android.BookAction; import com.orgzly.android.BookName; import com.orgzly.android.AppIntent; import com.orgzly.android.Filter; import com.orgzly.android.Note; import com.orgzly.android.NotesBatch; import com.orgzly.android.Shelf; import com.orgzly.android.prefs.AppPreferences; import com.orgzly.android.provider.clients.BooksClient; import com.orgzly.android.repos.Repo; import com.orgzly.android.repos.Rook; import com.orgzly.android.sync.SyncService; import com.orgzly.android.sync.SyncStatus; import com.orgzly.android.ui.CommonActivity; import com.orgzly.android.ui.NotePlace; import com.orgzly.android.ui.Place; import com.orgzly.android.util.AppPermissions; import com.orgzly.android.util.LogUtils; import com.orgzly.android.util.UriUtils; import com.orgzly.org.datetime.OrgDateTime; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Map; import java.util.Set; import java.util.TreeSet; /** * Retained fragment for sync button. FIXME: Cleanup * * Misused over time and now includes most async tasks. Move them and don't use a single listener * (check {@link com.orgzly.android.ui.ShareActivity}, it has to implement tons of methods with no reason) */ public class SyncFragment extends Fragment { private static final String TAG = SyncFragment.class.getName(); /** Name used for {@link android.app.FragmentManager}. */ public static final String FRAGMENT_TAG = SyncFragment.class.getName(); private boolean isServiceBound = false; /** Activity which has this fragment attached. Used as a target for hooks. */ private SyncFragmentListener mListener; private Shelf mShelf; /** Progress bar and button text. */ private SyncButton mSyncButton; private BroadcastReceiver syncServiceReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { SyncStatus status = SyncStatus.fromIntent(intent); if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, intent, status); /* Update sync button based on sync status. */ mSyncButton.update(status); switch (status.type) { case FAILED: if (mListener != null) { mListener.onSyncFinished(status.message); } break; case NO_STORAGE_PERMISSION: Activity activity = getActivity(); if (activity != null) { AppPermissions.isGrantedOrRequest((CommonActivity) activity, AppPermissions.FOR_SYNC_START); } break; case CANCELED: if (mListener != null) { /* No error message when sync is canceled by the user. */ mListener.onSyncFinished(null); } break; } } }; public static SyncFragment getInstance() { return new SyncFragment(); } /** * Mandatory empty constructor for the fragment manager to instantiate the * fragment (e.g. upon screen orientation changes). */ public SyncFragment() { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG); } /** * Hold a reference to the parent Activity so we can report the * task's current progress and results. The Android framework * will pass us a reference to the newly created Activity after * each configuration change. */ @Override public void onAttach(Context context) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, getActivity()); super.onAttach(context); /* This makes sure that the container activity has implemented * the callback interface. If not, it throws an exception */ try { mListener = (SyncFragmentListener) getActivity(); } catch (ClassCastException e) { throw new ClassCastException(getActivity().toString() + " must implement " + SyncFragmentListener.class); } mShelf = new Shelf(context.getApplicationContext()); } /** * This method will only be called once when the retained * Fragment is first created. */ @Override public void onCreate(Bundle savedInstanceState) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, savedInstanceState); super.onCreate(savedInstanceState); // Retain this fragment across configuration changes. setRetainInstance(true); LocalBroadcastManager.getInstance(getContext()) .registerReceiver(syncServiceReceiver, new IntentFilter(AppIntent.ACTION_SYNC_STATUS)); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, inflater, container, savedInstanceState); View view = inflater.inflate(R.layout.fragment_sync, container, false); /* Retained on configuration change. */ mSyncButton = new SyncButton(view, mSyncButton); return view; } @Override public void onStart() { super.onStart(); /* * Bind to sync service to request and receive the sync status. * We're doing this after button is initialized. */ bindToSyncService(); } @Override public void onStop() { super.onStop(); } @Override public void onDestroy() { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG); super.onDestroy(); LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(syncServiceReceiver); } /** * Set the callback to null so we don't accidentally leak the Activity instance. */ @Override public void onDetach() { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG); super.onDetach(); mListener = null; } /** * Load book from the Uri. */ public void importBookFromUri( final String bookName, final BookName.Format format, final Uri uri) { new AsyncTask<Void , Object, Object>() { @Override protected Object doInBackground(Void ... params) { /* Executing on a different thread. */ try { Book book; InputStream inputStream = getActivity().getContentResolver().openInputStream(uri); try { book = mShelf.loadBookFromStream(bookName, format, inputStream); } finally { inputStream.close(); } return book; } catch (IOException e) { e.printStackTrace(); return e; } } @Override protected void onPostExecute(Object result) { if (mListener != null) { if (result instanceof Book) { Book book = (Book) result; mShelf.setBookStatus(book, null, new BookAction(BookAction.Type.INFO, getString(R.string.imported))); mListener.onBookLoaded((Book) result); } else { mListener.onBookLoadFailed((IOException) result); } } } }.execute(); } /** * Load book from resource. * * FIXME: Only supports Org format (hardcoded below) */ public void loadBook(final String name, final Resources resources, final int resourceId) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, name, resources, resourceId); new AsyncTask<Void, Object, Object>() { @Override protected void onPreExecute() { } /* Executing on a different thread. */ @Override protected Object doInBackground(Void... params) { try { return mShelf.loadBookFromResource(name, BookName.Format.ORG, resources, resourceId); } catch (IOException e) { e.printStackTrace(); return e; } } @Override protected void onPostExecute(Object result) { if (mListener != null) { if (result instanceof Book) { Book book = (Book) result; // TODO: Do in bg mShelf.setBookStatus(book, null, new BookAction( BookAction.Type.INFO, getString(R.string.loaded_from_resource, name))); mListener.onBookLoaded(book); } else { // TODO: Why is status not updated here? mListener.onBookLoadFailed((IOException) result); } } } }.execute(); } /** * Load book from repository. */ public void loadBook(final long bookId) { new AsyncTask<Void, Object, Object>() { @Override protected Object doInBackground(Void... params) { Book book = BooksClient.get(getActivity(), bookId); try { if (book == null) { throw new IOException(getString(R.string.message_book_does_not_exist)); } Rook rook = book.getLink(); if (rook == null) { throw new IOException(getString(R.string.message_book_has_no_link)); } mShelf.setBookStatus( book, null, new BookAction( BookAction.Type.PROGRESS, getString(R.string.force_loading_from_uri, UriUtils.friendlyUri(rook.getUri())))); return mShelf.loadBookFromRepo(rook); } catch (Exception e) { e.printStackTrace(); mShelf.setBookStatus( book, null, new BookAction( BookAction.Type.ERROR, getString(R.string.force_loading_failed, e.getLocalizedMessage()))); return e; } } @Override protected void onPostExecute(Object result) { if (mListener != null) { if (result instanceof Book) { Book book = (Book) result; // TODO: Do in bg mShelf.setBookStatus( book, null, new BookAction( BookAction.Type.INFO, getString(R.string.force_loaded_from_uri, UriUtils.friendlyUri(book.getLastSyncedToRook().getUri())))); mListener.onBookLoaded((Book) result); } else { mListener.onBookLoadFailed((Exception) result); } } } }.execute(); } public void deleteFilters(final Set<Long> ids) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.deleteFilters(ids); return null; } }.execute(); } public void createFilter(final Filter filter) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.createFilter(filter); return null; } }.execute(); } public void updateFilter(final long id, final Filter filter) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.updateFilter(id, filter); return null; } }.execute(); } public void moveFilterUp(final long id) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.moveFilterUp(id); return null; } }.execute(); } public void moveFilterDown(final long id) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.moveFilterDown(id); return null; } }.execute(); } public void cycleVisibility(final Book book) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.cycleVisibility(book); return null; } }.execute(); } public void sparseTree(final long bookId, final long noteId) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { BooksClient.sparseTree(getContext(), bookId, noteId); return null; } }.execute(); } public void setStateToDone(final long noteId) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.setStateToDone(noteId); return null; } }.execute(); } public void promoteNotes(final long bookId, final Set<Long> noteIds) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.promoteNotes(bookId, noteIds); return null; } }.execute(); } public void demoteNotes(final long bookId, final Set<Long> noteIds) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.demoteNotes(bookId, noteIds); return null; } }.execute(); } /** * Saves book to its linked remote book, or to the one-and-only repository . */ public void forceSaveBook(final long bookId) { new AsyncTask<Void, Void, Object>() { @Override protected Object doInBackground(Void... params) { Book book = BooksClient.get(getActivity(), bookId); if (book != null) { try { String repoUrl; String fileName; /* Prefer link. */ if (book.getLink() != null) { Rook link = book.getLink(); repoUrl = link.getRepoUri().toString(); fileName = BookName.getFileName(getContext(), link.getUri()); } else { repoUrl = repoForSavingBook(); fileName = BookName.fileName(book.getName(), BookName.Format.ORG); } mShelf.setBookStatus(book, null, new BookAction(BookAction.Type.PROGRESS, getString(R.string.force_saving_to_uri, repoUrl))); return mShelf.saveBookToRepo(repoUrl, fileName, book, BookName.Format.ORG); } catch (Exception e) { e.printStackTrace(); mShelf.setBookStatus(book, null, new BookAction(BookAction.Type.ERROR, getString(R.string.force_saving_failed, e.getLocalizedMessage()))); return e; } } else { return new IOException(getString(R.string.message_book_does_not_exist)); } } @Override protected void onPostExecute(Object result) { if (mListener != null) { if (result instanceof Book) { Book book = (Book) result; mShelf.setBookStatus( book, null, new BookAction( BookAction.Type.INFO, getString(R.string.force_saved_to_uri, UriUtils.friendlyUri(book.getLastSyncedToRook().getUri())))); mListener.onBookSaved(book); } else { mListener.onBookForceSavingFailed((Exception) result); } } else { Log.w(TAG, "Listener not set, not handling saveBookToRepo result"); } } }.execute(); } /* If there is only one repository, return its URL. * If there are more, we don't know which one to use, so throw exception. */ private String repoForSavingBook() throws IOException { Map<String, Repo> repos = mShelf.getAllRepos(); /* Use repository if there is only one. */ if (repos.size() == 0) { throw new IOException(getString(R.string.no_repos)); } else if (repos.size() == 1) { return repos.keySet().iterator().next(); } else { throw new IOException(getString(R.string.multiple_repos)); } } /** * Exports book. Link is not updated, book stays linked to the same remote book. */ public void exportBook(final long bookId) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, bookId); new AsyncTask<Void, Void, Object>() { @Override protected Object doInBackground(Void... params) { try { return mShelf.exportBook(bookId, BookName.Format.ORG); } catch (IOException e) { e.printStackTrace(); return e; } } @Override protected void onPostExecute(Object result) { if (mListener != null) { if (result instanceof File) { mListener.onBookExported((File) result); } else { mListener.onBookExportFailed((IOException) result); } } else { Log.w(TAG, "Listener not set, not handling exportBook result"); } } }.execute(); } public void clearDatabase() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.clearDatabase(); return null; } @Override protected void onPostExecute(Void aVoid) { if (mListener != null) { mListener.onDatabaseCleared(); } else { Log.w(TAG, "Listener not set, not calling onDatabaseCleared"); } } }.execute(); } public void deleteBook(final Book book, final boolean deleteLinked) { new AsyncTask<Void, Void, Object>() { @Override protected Object doInBackground(Void... params) { try { mShelf.deleteBook(book, deleteLinked); return null; /* Success. */ } catch (IOException e) { e.printStackTrace(); return e; } } @Override protected void onPostExecute(Object result) { if (mListener != null) { if (result == null) { mListener.onBookDeleted(book); } else { mListener.onBookDeletingFailed(book, (IOException) result); } } else { Log.w(TAG, "Listener not set, not calling onBookDeleted"); } } }.execute(); } public void createNewBook(final String name) { new AsyncTask<Void, Void, Object>() { @Override protected Object doInBackground(Void... params) { try { Book book = mShelf.createBook(name); mShelf.setBookStatus(book, null, new BookAction(BookAction.Type.INFO, getString(R.string.created))); return book; } catch (IOException e) { e.printStackTrace(); return e; } } @Override protected void onPostExecute(Object result) { if (mListener != null) { if (result instanceof Book) { mListener.onBookCreated((Book) result); } else { mListener.onBookCreationFailed((IOException) result); } } else { Log.w(TAG, "Listener not set, not handling createNewBook result"); } } }.execute(); } public void updateScheduledTime(final Set<Long> noteIds, final OrgDateTime time) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.setNotesScheduledTime(noteIds, time); return null; } @Override protected void onPostExecute(Void aVoid) { if (mListener != null) { mListener.onScheduledTimeUpdated(noteIds, time); } else { Log.w(TAG, "Listener not set, not calling onScheduledTimeUpdated"); } } }.execute(); } public void updateNoteState(final Set<Long> noteIds, final String state) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.setNotesState(noteIds, state); return null; } @Override protected void onPostExecute(Void aVoid) { if (mListener != null) { mListener.onStateChanged(noteIds, state); } else { Log.w(TAG, "Listener not set, not calling onStateChanged"); } } }.execute(); } public void shiftNoteState(final long id, final int direction) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.shiftState(id, direction); return null; } @Override protected void onPostExecute(Void aVoid) { if (mListener == null) { Log.w(TAG, "Listener not set, not calling onStateChanged"); } } }.execute(); } public void onSyncButton() { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG); Intent intent = new Intent(getActivity(), SyncService.class); getActivity().startService(intent); } public void renameBook(final Book book, final String value) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { try { mShelf.renameBook(book, value); } catch (Exception e) { e.printStackTrace(); mShelf.setBookStatus(book, null, new BookAction( BookAction.Type.ERROR, getString(R.string.failed_renaming_book_with_reason, e.getLocalizedMessage()))); // return e; } return null; } }.execute(); } public void updateBookSettings(final Book book) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mShelf.updateBookSettings(book); return null; } }.execute(); } /* * Sync button which should be updated from the main UI thread. */ private class SyncButton { private final Context appContext; private final ProgressBar progressBar; private final ViewGroup buttonContainer; private final TextView buttonText; private final View buttonIcon; private final Animation rotation; public SyncButton(View view, SyncButton prev) { this.appContext = getActivity().getApplicationContext(); rotation = AnimationUtils.loadAnimation(appContext, R.anim.rotate_counterclockwise); rotation.setRepeatCount(Animation.INFINITE); progressBar = (ProgressBar) view.findViewById(R.id.sync_progress_bar); buttonContainer = (ViewGroup) view.findViewById(R.id.sync_button_container); buttonText = (TextView) view.findViewById(R.id.sync_button_text); buttonIcon = view.findViewById(R.id.sync_button_icon); if (prev != null) { /* Restore old state. */ progressBar.setIndeterminate(prev.progressBar.isIndeterminate()); progressBar.setMax(prev.progressBar.getMax()); progressBar.setProgress(prev.progressBar.getProgress()); progressBar.setVisibility(prev.progressBar.getVisibility()); buttonText.setText(prev.buttonText.getText()); } else { progressBar.setVisibility(View.GONE); setButtonTextToLastSynced(); } buttonContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onSyncButton(); } }); buttonContainer.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { new AlertDialog.Builder(getContext()) .setPositiveButton(R.string.ok, null) .setMessage(buttonText.getText()) .show(); return true; } }); } private void setButtonTextToLastSynced() { long time = AppPreferences.lastSuccessfulSyncTime(appContext); if (time > 0) { buttonText.setText(getString(R.string.last_sync_prefix, formatLastSyncTime(time))); } else { buttonText.setText(R.string.sync); } } public void update(SyncStatus status) { switch (status.type) { case STARTING: progressBar.setIndeterminate(true); progressBar.setVisibility(View.VISIBLE); setAnimation(true); buttonText.setText(R.string.syncing_in_progress); break; case CANCELING: progressBar.setIndeterminate(true); progressBar.setVisibility(View.VISIBLE); setAnimation(true); buttonText.setText(R.string.canceling); break; case BOOKS_COLLECTED: progressBar.setIndeterminate(false); progressBar.setMax(status.totalBooks); progressBar.setProgress(0); progressBar.setVisibility(View.VISIBLE); setAnimation(true); buttonText.setText(R.string.syncing_in_progress); break; case BOOK_STARTED: progressBar.setIndeterminate(false); progressBar.setMax(status.totalBooks); progressBar.setProgress(status.currentBook); progressBar.setVisibility(View.VISIBLE); setAnimation(true); buttonText.setText(getString(R.string.syncing_book, status.message)); break; case BOOK_ENDED: progressBar.setIndeterminate(false); progressBar.setMax(status.totalBooks); progressBar.setProgress(status.currentBook); progressBar.setVisibility(View.VISIBLE); setAnimation(true); buttonText.setText(R.string.syncing_in_progress); break; case NOT_RUNNING: case FINISHED: progressBar.setVisibility(View.GONE); setAnimation(false); setButtonTextToLastSynced(); break; case CANCELED: case FAILED: progressBar.setVisibility(View.GONE); setAnimation(false); buttonText.setText(getString(R.string.last_sync_prefix, status.message)); break; } } private void setAnimation(boolean shouldAnimate) { if (shouldAnimate) { if (buttonIcon.getAnimation() == null) { buttonIcon.startAnimation(rotation); } } else { if (buttonIcon.getAnimation() != null) { buttonIcon.clearAnimation(); } } } private String formatLastSyncTime(long time) { return DateUtils.formatDateTime( appContext, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_TIME); } } public void updateNote(final Note note) { new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... dummy) { return mShelf.updateNote(note); } @Override protected void onPostExecute(Integer noOfUpdated) { if (noOfUpdated == 1) { mListener.onNoteUpdated(note); } else { mListener.onNoteUpdatingFailed(note); } } }.execute(); } public void createNote(final Note note, final NotePlace notePlace) { new AsyncTask<Void, Void, Note>() { @Override protected Note doInBackground(Void... dummy) { return mShelf.createNote(note, notePlace); } @Override protected void onPostExecute(Note createdNote) { if (createdNote != null) { if (AppPreferences.syncAfterNewNoteCreated(getContext())) { Intent intent = new Intent(getActivity(), SyncService.class); intent.setAction(AppIntent.ACTION_SYNC_START); getActivity().startService(intent); } mListener.onNoteCreated(createdNote); } else { mListener.onNoteCreatingFailed(); } } }.execute(); } /** * Delete notes from the notebook asynchronously. * Calls {@link SyncFragmentListener#onNotesDeleted(int)}. * * @param bookId Book ID * @param noteIds Set of notes' IDs */ public void deleteNotes(final long bookId, final TreeSet<Long> noteIds) { new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... params) { return mShelf.delete(bookId, noteIds); } @Override protected void onPostExecute(Integer result) { mListener.onNotesDeleted(result); } }.execute(); } public void deleteNotes(long bookId, long noteId) { TreeSet<Long> noteIds = new TreeSet<>(); noteIds.add(noteId); deleteNotes(bookId, noteIds); } /** * Cut notes from the notebook asynchronously. * Calls {@link SyncFragmentListener#onNotesCut(int)}. * * @param bookId Book ID * @param noteIds Set of notes' IDs */ public void cutNotes(final long bookId, final TreeSet<Long> noteIds) { new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... params) { return mShelf.cut(bookId, noteIds); } @Override protected void onPostExecute(Integer result) { mListener.onNotesCut(result); } }.execute(); } public void pasteNotes(final long bookId, final long noteId, final Place place) { new AsyncTask<Void, Void, NotesBatch>() { @Override protected NotesBatch doInBackground(Void... voids) { return mShelf.paste(bookId, noteId, place); } @Override protected void onPostExecute(NotesBatch batch) { if (batch != null) { mListener.onNotesPasted(batch); } else { mListener.onNotesNotPasted(); } } }.execute(); } /** * Re-parsing notes currently only checks for notes' title and state. */ public void reParseNotes() { new AsyncTask<Void, Object, IOException>() { private ProgressDialog progressDialog; @Override protected void onPreExecute() { progressDialog = new ProgressDialog(getActivity()); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setMessage(getString(R.string.updating_notes)); progressDialog.show(); } @Override protected IOException doInBackground(Void[] params) { try { mShelf.reParseNotesStateAndTitles(new Shelf.ReParsingNotesListener() { @Override public void noteParsed(int current, int total, String msg) { publishProgress(current, total, msg); } }); } catch (IOException e) { return e; } return null; /* Success. */ } @Override protected void onPostExecute(IOException exception) { /* * If dialog is gone due to rotation for example, IllegalArgumentException occurs * here on dismiss() (and isShowing() returns true). * Catch & ignore - http://stackoverflow.com/questions/2745061/java-lang-illegalargumentexception-view-not-attached-to-window-manager */ try { if (progressDialog.isShowing()) { progressDialog.dismiss(); } } catch (Exception e) { } /* TODO: Do this for all other errors as well? */ if (exception != null) { new AlertDialog.Builder(getActivity()) .setTitle(R.string.failure) .setMessage(exception.toString()) .setPositiveButton(R.string.ok, null) .show(); } } @Override protected void onProgressUpdate(Object ... values) { int current = (Integer) values[0]; int total = (Integer) values[1]; String msg = (String) values[2]; progressDialog.setMessage(msg); if (total == 0) { progressDialog.setIndeterminate(true); } else { progressDialog.setIndeterminate(false); progressDialog.setProgress(current); progressDialog.setMax(total); } } }.execute(); } private void bindToSyncService() { Intent intent = new Intent(getActivity(), SyncService.class); Activity activity = getActivity(); if (activity != null) { activity.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); } } private void unbindFromSyncService() { if (isServiceBound) { Activity activity = getActivity(); if (activity != null) { activity.unbindService(serviceConnection); isServiceBound = false; } } } private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder serviceBinder) { isServiceBound = true; SyncService.LocalBinder binder = (SyncService.LocalBinder) serviceBinder; /* * Check for activity added due to tests sometimes triggering: * java.lang.IllegalStateException: Fragment SyncFragment{782d3f6} not attached to Activity * Probably not specific to tests. */ if (getActivity() != null) { /* Get current sync status from the service and update the button. */ SyncStatus status = binder.getService().getStatus(); mSyncButton.update(status); } unbindFromSyncService(); } @Override public void onServiceDisconnected(ComponentName arg0) { isServiceBound = false; } }; public interface SyncFragmentListener { void onBookCreated(Book book); void onBookCreationFailed(Exception exception); void onBookLoaded(Book book); void onBookLoadFailed(Exception exception); void onBookSaved(Book book); void onBookForceSavingFailed(Exception exception); void onSyncFinished(String msg); void onBookExported(File file); void onBookExportFailed(Exception exception); void onNotesPasted(NotesBatch batch); void onNotesNotPasted(); void onDatabaseCleared(); void onBookDeleted(Book book); void onBookDeletingFailed(Book book, IOException exception); void onScheduledTimeUpdated(Set<Long> noteIds, OrgDateTime time); void onStateChanged(Set<Long> noteIds, String state); void onNoteCreated(Note note); void onNoteCreatingFailed(); void onNoteUpdated(Note note); void onNoteUpdatingFailed(Note note); void onNotesDeleted(int count); void onNotesCut(int count); } }