package com.dropbox.examples.notes; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.concurrent.Semaphore; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.Fragment; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.TextView; import com.dropbox.sync.android.DbxAccount; import com.dropbox.sync.android.DbxException; import com.dropbox.sync.android.DbxFile; import com.dropbox.sync.android.DbxFileSystem; import com.dropbox.sync.android.DbxPath; public class NoteDetailFragment extends Fragment { private static final String TAG = NoteDetailFragment.class.getName(); private static final String ARG_PATH = "path"; private EditText mText; private TextView mErrorMessage; private View mOldVersionWarningView; private View mLoadingSpinner; private final DbxLoadHandler mHandler = new DbxLoadHandler(this); private DbxFile mFile; private final Object mFileLock = new Object(); private final Semaphore mFileUseSemaphore = new Semaphore(1); private boolean mUserHasModifiedText = false; private boolean mHasLoadedAnyData = false; private final DbxFile.Listener mChangeListener = new DbxFile.Listener() { @Override public void onFileChange(DbxFile file) { if (mUserHasModifiedText) { // User has modified the text locally, so we no longer care // about external changes. return; } boolean currentIsLatest; boolean newerIsCached = false; try { currentIsLatest = file.getSyncStatus().isLatest; if (!currentIsLatest) { newerIsCached = file.getNewerStatus().isCached; } } catch (DbxException e) { Log.w(TAG, "Failed to get sync status", e); return; } mHandler.sendIsShowingLatestMessage(currentIsLatest); // kick off an update if necessary if (newerIsCached || !mHasLoadedAnyData) { mHandler.sendDoUpdateMessage(); } } }; public NoteDetailFragment() {} public static NoteDetailFragment getInstance(DbxPath path) { NoteDetailFragment fragment = new NoteDetailFragment(); Bundle args = new Bundle(); args.putString(ARG_PATH, path.toString()); fragment.setArguments(args); return fragment; } @Override public void onStart() { super.onStart(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_note_detail, container, false); mText = (EditText)view.findViewById(R.id.note_detail); mText.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { mUserHasModifiedText = true; } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void afterTextChanged(Editable s) {} }); mOldVersionWarningView = view.findViewById(R.id.old_version); mLoadingSpinner = view.findViewById(R.id.note_loading); mErrorMessage = (TextView)view.findViewById(R.id.error_message); return view; } @Override public void onResume() { super.onResume(); mText.setEnabled(false); mText.setText(""); mUserHasModifiedText = false; mHasLoadedAnyData = false; DbxPath path = new DbxPath(getArguments().getString(ARG_PATH)); // Grab the note name from the path: String title = Util.stripExtension("txt", path.getName()); getActivity().setTitle(title); DbxAccount acct = NotesAppConfig.getAccountManager(getActivity()).getLinkedAccount(); if (null == acct) { Log.e(TAG, "No linked account."); return; } mErrorMessage.setVisibility(View.GONE); mLoadingSpinner.setVisibility(View.VISIBLE); /* * Since mFile is written asynchronously after onPause, it's possible * that the activity is resumed again before a write finishes. This * semaphore prevents us from trying to re-open the file while it's * still being written in the background - we hold it whenever mFile is * in use, and release it when the write is finished and we're done with * the file. */ try { mFileUseSemaphore.acquire(); } catch (InterruptedException e) { throw new RuntimeException(e); } try { DbxFileSystem fs = DbxFileSystem.forAccount(acct); try { mFile = fs.open(path); } catch (DbxException.NotFound e) { mFile = fs.create(path); } } catch (DbxException e) { Log.e(TAG, "failed to open or create file.", e); return; } mFile.addListener(mChangeListener); boolean latest; try { latest = mFile.getSyncStatus().isLatest; } catch (DbxException e) { Log.w(TAG, "Failed to get sync status", e); return; } mHandler.sendIsShowingLatestMessage(latest); mHandler.sendDoUpdateMessage(); } @Override public void onPause() { super.onPause(); mFile.removeListener(mChangeListener); // If the contents have changed, write them back to Dropbox if (mUserHasModifiedText && mFile != null) { final String newContents = mText.getText().toString(); mUserHasModifiedText = false; // Start a thread to do the write. new Thread(new Runnable() { @Override public void run() { Log.d(TAG, "starting write"); synchronized (mFileLock) { try { mFile.writeString(newContents); } catch (IOException e) { Log.e(TAG, "failed to write to file", e); } } mFile.close(); Log.d(TAG, "write done"); mFile = null; mFileUseSemaphore.release(); } }).start(); } else { mFile.close(); mFile = null; mFileUseSemaphore.release(); } } private void startUpdateOnBackgroundThread() { new Thread(new Runnable() { @Override public void run() { synchronized (mFileLock) { boolean updated; try { updated = mFile.update(); } catch (DbxException e) { Log.e(TAG, "failed to update file", e); mHandler.sendLoadFailedMessage(e.toString()); return; } boolean isCached; try { isCached = mFile.getSyncStatus().isCached; } catch (DbxException e) { Log.e(TAG, "failed to get sync status", e); mHandler.sendLoadFailedMessage(e.toString()); return; } if (!mHasLoadedAnyData || updated) { Log.d(TAG, "starting read"); String contents; try { contents = mFile.readString(); } catch (IOException e) { Log.e(TAG, "failed to read file", e); if (!mHasLoadedAnyData) { mHandler.sendLoadFailedMessage(getString(R.string.error_failed_load)); } return; } Log.d(TAG, "read done"); if (contents != null) { mHasLoadedAnyData = true; } mHandler.sendUpdateDoneWithChangesMessage(contents); } else { mHandler.sendUpdateDoneWithoutChangesMessage(); } } } }).start(); } private void applyNewText(final String data) { if (mUserHasModifiedText || data == null) { return; } mText.setText(data); mText.setEnabled(true); // explicitly reset mChanged to false since the setText above changed it to true mUserHasModifiedText = false; } private static class DbxLoadHandler extends Handler { private final WeakReference<NoteDetailFragment> mFragment; public static final int MESSAGE_IS_SHOWING_LATEST = 0; public static final int MESSAGE_DO_UPDATE = 1; public static final int MESSAGE_UPDATE_DONE = 2; public static final int MESSAGE_LOAD_FAILED = 3; public DbxLoadHandler(NoteDetailFragment containingFragment) { mFragment = new WeakReference<NoteDetailFragment>(containingFragment); } @Override public void handleMessage(Message msg) { NoteDetailFragment frag = mFragment.get(); if (frag == null) { return; } if (msg.what == MESSAGE_IS_SHOWING_LATEST) { boolean latest = msg.arg1 != 0; frag.mOldVersionWarningView.setVisibility(latest ? View.GONE : View.VISIBLE); } else if (msg.what == MESSAGE_DO_UPDATE) { if (frag.mUserHasModifiedText) { // user has made changes to the file, so ignore this request return; } // disable UI before doing an update - if user were to make // changes between now and when the update completes, they would // erroneously be applied on top of that newer version, so // prevent that by just temporarily disabling the UI (should be // quick anyway). frag.mText.setEnabled(false); frag.startUpdateOnBackgroundThread(); } else if (msg.what == MESSAGE_UPDATE_DONE) { if (frag.mUserHasModifiedText) { Log.wtf(TAG, "Somehow user changed text while an update was in progress!"); } frag.mText.setVisibility(View.VISIBLE); frag.mLoadingSpinner.setVisibility(View.GONE); frag.mErrorMessage.setVisibility(View.GONE); boolean gotNewData = msg.arg1 != 0; if (gotNewData) { String contents = (String)msg.obj; frag.applyNewText(contents); } frag.mText.requestFocus(); // reenable the UI frag.mText.setEnabled(true); } else if (msg.what == MESSAGE_LOAD_FAILED) { String errorText = (String)msg.obj; frag.mText.setVisibility(View.GONE); frag.mLoadingSpinner.setVisibility(View.GONE); frag.mErrorMessage.setText(errorText); frag.mErrorMessage.setVisibility(View.VISIBLE); } else { throw new RuntimeException("Unknown message"); } } public void sendIsShowingLatestMessage(boolean isLatestVersion) { sendMessage(Message.obtain(this, MESSAGE_IS_SHOWING_LATEST, isLatestVersion ? 1 : 0, -1)); } public void sendDoUpdateMessage() { sendMessage(Message.obtain(this, MESSAGE_DO_UPDATE)); } public void sendUpdateDoneWithChangesMessage(String newContents) { sendMessage(Message.obtain(this, MESSAGE_UPDATE_DONE, 1, -1, newContents)); } public void sendUpdateDoneWithoutChangesMessage() { sendMessage(Message.obtain(this, MESSAGE_UPDATE_DONE, 0, -1)); } public void sendLoadFailedMessage(String errorText) { sendMessage(Message.obtain(this, MESSAGE_LOAD_FAILED, errorText)); } } }