Wednesday, September 30, 2015

My Notes: Observer Pattern

Android provides the components to implement the Observer pattern.  For those unfamiliar with the pattern it is a way to notify observers when something changes in the observable object.  This is actually a great thing to use when monitoring data and works really well in conjunction with Loaders. 

The important classes are named Observer and Observable. The item being observed extends the Observable class.  Whenever observed data is changed, the Observable calls the setChanged method.  It can then call notifyObservers to notify any registered observers.  It is very important that the setChanged method be called first, otherwise the observers are never notified about a change in the data.

The Observers will implement the Observer interface.  This requires the class to implement the update method, which is called in response to a changed the observable object.

For me the obvious case for the pattern is the Note class.  I want to modify the UI in response to changes in the note color, text, or saved status.  The easiest way to do this is to turn the Note objects into Observables. 

   1: package com.fsk.mynotes.data;
   2:  
   3:  
   4: import android.content.ContentValues;
   5: import android.database.sqlite.SQLiteDatabase;
   6: import android.os.Parcel;
   7: import android.os.Parcelable;
   8: import android.support.annotation.NonNull;
   9: import android.util.Log;
  10:  
  11: import com.fsk.common.database.DatabaseStorable;
  12: import com.fsk.common.threads.ThreadUtils;
  13: import com.fsk.mynotes.constants.NoteColor;
  14: import com.fsk.mynotes.data.database.MyNotesDatabaseModel.Columns;
  15: import com.fsk.mynotes.data.database.MyNotesDatabaseModel.Tables;
  16: import com.google.common.base.Preconditions;
  17:  
  18: import java.util.Observable;
  19:  
  20:  
  21: /**
  22:  * The Note data model.
  23:  */
  24: public class Note extends Observable implements Parcelable, DatabaseStorable {
  25:  
  26:     /**
  27:      * Standard Creator Pattern
  28:      */
  29:     public static final Parcelable.Creator<Note> CREATOR = new Parcelable.Creator<Note>() {
  30:         @Override
  31:         public Note createFromParcel(final Parcel source) {
  32:             return new Note(source);
  33:         }
  34:  
  35:  
  36:         @Override
  37:         public Note[] newArray(final int size) {
  38:             return new Note[size];
  39:         }
  40:     };
  41:  
  42:  
  43:     /**
  44:      * The id for notes that are not stored in the database.
  45:      */
  46:     public static final int NOT_STORED = NoteAttributes.UNKNOWN;
  47:  
  48:  
  49:     /**
  50:      * The current data for the note.
  51:      */
  52:     private NoteAttributes mCurrentData;
  53:  
  54:  
  55:     /**
  56:      * The last persisted data for the note
  57:      */
  58:     private NoteAttributes mOriginalData;
  59:  
  60:  
  61:     /**
  62:      * Constructor
  63:      *
  64:      * @param startingData
  65:      *         the initial starting data for the note.
  66:      *
  67:      * @throws CloneNotSupportedException
  68:      *         when the starting data cannot be cloned.
  69:      */
  70:     Note(@NonNull NoteAttributes startingData) throws CloneNotSupportedException {
  71:         Preconditions.checkNotNull(startingData);
  72:  
  73:         mOriginalData = startingData;
  74:         mCurrentData = startingData.clone();
  75:     }
  76:  
  77:  
  78:     /**
  79:      * Constructor.
  80:      *
  81:      * @param source
  82:      *         the parcel containing the data for the note.
  83:      */
  84:     public Note(Parcel source) {
  85:         mOriginalData = source.readParcelable(NoteAttributes.class.getClassLoader());
  86:         mCurrentData = source.readParcelable(NoteAttributes.class.getClassLoader());
  87:     }
  88:  
  89:  
  90:     /**
  91:      * Get the note color.
  92:      *
  93:      * @return The non-null note color.  By default, this returns {@link NoteColor#YELLOW}.
  94:      */
  95:     public NoteColor getColor() {
  96:         return mCurrentData.getColor();
  97:     }
  98:  
  99:  
 100:     /**
 101:      * Set the color for the note.
 102:      *
 103:      * @param color
 104:      *         the color for the note.
 105:      */
 106:     public void setColor(@NonNull NoteColor color) {
 107:         boolean modified = mCurrentData.setColor(color);
 108:         notifyObserversOnChange(modified);
 109:     }
 110:  
 111:  
 112:     /**
 113:      * Get the text for the note.
 114:      *
 115:      * @return the non-null text for the note.
 116:      */
 117:     public String getText() {
 118:         return mCurrentData.getText();
 119:     }
 120:  
 121:  
 122:     /**
 123:      * Set the note text.
 124:      *
 125:      * @param text
 126:      *         the text for the note.  If null, then an empty string is set.
 127:      */
 128:     public void setText(String text) {
 129:         boolean modified = mCurrentData.setText(text);
 130:         notifyObserversOnChange(modified);
 131:     }
 132:  
 133:  
 134:     /**
 135:      * Get the id for the note.
 136:      *
 137:      * @return the id for the note.
 138:      */
 139:     public long getId() {
 140:         return mCurrentData.getId();
 141:     }
 142:  
 143:  
 144:     /**
 145:      * Determine if the Note has been modified without the changes being persisted.
 146:      *
 147:      * @return true if the Note has been modified without the changes being persisted.
 148:      */
 149:     public boolean isDirty() {
 150:         return !mCurrentData.equals(mOriginalData);
 151:     }
 152:  
 153:  
 154:     /**
 155:      * Notify any {@link java.util.Observer}(s) of change to the data.
 156:      *
 157:      * @param changed
 158:      *         true if the data was changed.
 159:      */
 160:     private void notifyObserversOnChange(boolean changed) {
 161:         if (changed) {
 162:             setChanged();
 163:             notifyObservers();
 164:         }
 165:     }
 166:  
 167:  
 168:     @Override
 169:     public int describeContents() {
 170:         return 0;
 171:     }
 172:  
 173:  
 174:     @Override
 175:     public void writeToParcel(final Parcel dest, final int flags) {
 176:         dest.writeParcelable(mOriginalData, flags);
 177:         dest.writeParcelable(mCurrentData, flags);
 178:     }
 179:  
 180:  
 181:     /**
 182:      * Convert the object into a {@link android.content.ContentValues} object for storage in the
 183:      * database.
 184:      *
 185:      * @return The {@link android.content.ContentValues} initialized with the values.  It will
 186:      * contain the following entries:<p> key=>{@link com.fsk.mynotes.data.database
 187:      * .MyNotesDatabaseModel.Columns#NOTE_ID}<br> value=> long<br> description=> The note id.  this
 188:      * only exists when {@link #getId()} is not {@link #NOT_STORED}.
 189:      * <p/>
 190:      * key=>{@link com.fsk.mynotes.data.database.MyNotesDatabaseModel.Columns#NOTE_TEXT}<br>
 191:      * value=>String<br> description=> The note's text.
 192:      * <p/>
 193:      * key=>{@link com.fsk.mynotes.data.database.MyNotesDatabaseModel.Columns#NOTE_COLOR}<br>
 194:      * value=>Integer<br> description=> The note colors ordinal.
 195:      */
 196:     ContentValues createContentValues() {
 197:         ContentValues returnValue = new ContentValues();
 198:         if (getId() != NOT_STORED) {
 199:             returnValue.put(Columns.NOTE_ID, getId());
 200:         }
 201:  
 202:         returnValue.put(Columns.NOTE_TEXT, getText());
 203:         returnValue.put(Columns.NOTE_COLOR, getColor().ordinal());
 204:  
 205:         return returnValue;
 206:     }
 207:  
 208:  
 209:     /**
 210:      * Save the note to the database.
 211:      *
 212:      * @throws com.fsk.common.threads.ThreadException
 213:      *         when call from the UI thread.
 214:      */
 215:     @Override
 216:     public void save(final SQLiteDatabase db) {
 217:         new ThreadUtils().checkOffUIThread();
 218:         long row = db.insertWithOnConflict(Tables.NOTES, null, createContentValues(),
 219:                                            SQLiteDatabase.CONFLICT_REPLACE);
 220:  
 221:         if (row != NOT_STORED) {
 222:             onPersistenceUpdate(row);
 223:         }
 224:     }
 225:  
 226:  
 227:     /**
 228:      * Delete the note from the database.
 229:      *
 230:      * @throws com.fsk.common.threads.ThreadException
 231:      *         when call from the UI thread.
 232:      */
 233:     @Override
 234:     public void delete(final SQLiteDatabase db) {
 235:  
 236:         new ThreadUtils().checkOffUIThread();
 237:  
 238:         if (getId() != NOT_STORED) {
 239:             int deletedRows = db.delete(Tables.NOTES, Columns.NOTE_ID + " = ?",
 240:                                         new String[] { Long.toString(getId()) });
 241:             if (deletedRows > 0) {
 242:                 onPersistenceUpdate(NOT_STORED);
 243:             }
 244:         }
 245:     }
 246:  
 247:  
 248:     /**
 249:      * Reset the {@link #mOriginalData} to the current and update the id to the specified id.
 250:      *
 251:      * @param newId
 252:      *         the new Id for the note.
 253:      */
 254:     private void onPersistenceUpdate(long newId) {
 255:         try {
 256:             mCurrentData.setId(newId);
 257:             boolean dirty = isDirty();
 258:             mOriginalData = mCurrentData.clone();
 259:             notifyObserversOnChange(dirty);
 260:         }
 261:         catch (CloneNotSupportedException e) {
 262:             Log.i("MyNotes", "Something really bad happened");
 263:         }
 264:     }
 265:  
 266:  
 267:     /**
 268:      * The Builder that will create a new {@link Note}.
 269:      */
 270:     public static class Builder {
 271:         /**
 272:          * The starting data for the new {@link Note}.
 273:          */
 274:         private final NoteAttributes mNoteAttributes = new NoteAttributes();
 275:  
 276:  
 277:         /**
 278:          * Set the color for the note.
 279:          *
 280:          * @param color
 281:          *         the color for the note.
 282:          *
 283:          * @return the builder to allow method chaining.
 284:          */
 285:         public Builder setColor(@NonNull NoteColor color) {
 286:             mNoteAttributes.setColor(color);
 287:             return this;
 288:         }
 289:  
 290:  
 291:         /**
 292:          * Set the note text.
 293:          *
 294:          * @param text
 295:          *         the text for the note.  If null, then an empty string is set.
 296:          *
 297:          * @return the builder to allow method chaining.
 298:          */
 299:         public Builder setText(String text) {
 300:             mNoteAttributes.setText(text);
 301:             return this;
 302:         }
 303:  
 304:  
 305:         /**
 306:          * Set the note id.
 307:          *
 308:          * @param id
 309:          *         the id for the note.  This must be {@link #NOT_STORED} or a natural number.
 310:          *
 311:          * @return the builder to allow method chaining.
 312:          */
 313:         public Builder setId(long id) {
 314:             mNoteAttributes.setId(id);
 315:             return this;
 316:         }
 317:  
 318:  
 319:         /**
 320:          * Create a new {@link Note} with the specified data.
 321:          *
 322:          * @return a new {@link Note} with the specified data.
 323:          *
 324:          * @throws CloneNotSupportedException
 325:          *         when the note data cannot be created.
 326:          */
 327:         public Note build() throws CloneNotSupportedException {
 328:             return new Note(mNoteAttributes);
 329:         }
 330:     }
 331: }

I created a method, notifyObserversOfChange that centralizes the setChanged and notifyObservers method calls.  The caller decides if the data has actually changed.   This method is called whenever the text, color, or peristenance status changes.



   1: /**
   2:      * Notify any {@link java.util.Observer}(s) of change to the data.
   3:      *
   4:      * @param changed
   5:      *         true if the data was changed.
   6:      */
   7:     private void notifyObserversOnChange(boolean changed) {
   8:         if (changed) {
   9:             setChanged();
  10:             notifyObservers();
  11:         }
  12:     }

I then setup the FilteredNoteLoader to observe the note data.   That way when any note changes, it can be automatically reflected in the UI list of notes.



   1: public class FilteredNoteLoader extends AsyncTaskLoader<List<Note>> implements Observer {
   2:  
   3:     /**
   4:      * The filtered notes retrieved from persistent storage.
   5:      */
   6:     private List<Note> mNotes;
   7:  
   8:  
   9:     /**
  10:      * The notes manager that retrieves data from persistent storage.
  11:      */
  12:     private final NotesManager mNotesManager;
  13:  
  14:  
  15:     /**
  16:      * The filter manager that retrieves data from persistent storage.
  17:      */
  18:     private final NoteFilterPreferences mNoteFilterPreferences;
  19:  
  20:  
  21:     /**
  22:      * A flag that indicates that the loader is monitoring changes to the notes or note filter.
  23:      */
  24:     private boolean mObserverRegistered;
  25:  
  26:  
  27:     /**
  28:      * The listener to changes in the filtered note colors.
  29:      */
  30:     private SharedPreferences.OnSharedPreferenceChangeListener mNoteFilterPreferenceListener =
  31:             new SharedPreferences.OnSharedPreferenceChangeListener() {
  32:                 @Override
  33:                 public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
  34:                                                       final String key) {
  35:                     onContentChanged();
  36:                 }
  37:             };
  38:  
  39:  
  40:     /**
  41:      * Constructor.
  42:      *
  43:      * @param context
  44:      *         The context to associate with this loader.
  45:      */
  46:     public FilteredNoteLoader(@NonNull Context context) {
  47:         super(context);
  48:  
  49:         mNotesManager = new NotesManager(DatabaseHelper.getDatabase());
  50:         mNoteFilterPreferences = new NoteFilterPreferences(context);
  51:     }
  52:  
  53:  
  54:     @Override
  55:     public List<Note> loadInBackground() {
  56:         Set<NoteColor> enabledFilters = mNoteFilterPreferences.getEnabledColors();
  57:         List<Note> returnValue;
  58:         try {
  59:             returnValue = mNotesManager.getNotesWithColors(new ArrayList<>(enabledFilters));
  60:         }
  61:         catch (Exception e) {
  62:             returnValue = null;
  63:         }
  64:         return returnValue;
  65:     }
  66:  
  67:  
  68:     @Override
  69:     public void deliverResult(List<Note> data) {
  70:         if (isReset()) {
  71:             return;
  72:         }
  73:  
  74:         mNotes = data;
  75:  
  76:         if (isStarted()) {
  77:             super.deliverResult(data);
  78:         }
  79:     }
  80:  
  81:  
  82:     @Override
  83:     protected void onStartLoading() {
  84:         if (mNotes != null) {
  85:             deliverResult(mNotes);
  86:         }
  87:  
  88:         registerReceivers();
  89:  
  90:         if (takeContentChanged() || mNotes == null) {
  91:             forceLoad();
  92:         }
  93:     }
  94:  
  95:  
  96:     @Override
  97:     protected void onStopLoading() {
  98:         cancelLoad();
  99:     }
 100:  
 101:  
 102:     @Override
 103:     protected void onReset() {
 104:         onStopLoading();
 105:  
 106:         if (mNotes != null) {
 107:             mNotes = null;
 108:         }
 109:  
 110:         unregisterReceivers();
 111:     }
 112:  
 113:  
 114:     @Override
 115:     public void update(final Observable observable, final Object data) {
 116:         onContentChanged();
 117:     }
 118:  
 119:  
 120:     /**
 121:      * Register the observer to monitor changes to the note or note filter.
 122:      */
 123:     private void registerReceivers() {
 124:         if (!mObserverRegistered) {
 125:             mNoteFilterPreferences.registerListener(mNoteFilterPreferenceListener);
 126:  
 127:             for (Note note : MoreObjects.firstNonNull(mNotes, new ArrayList<Note>())) {
 128:                 note.addObserver(this);
 129:             }
 130:  
 131:             mObserverRegistered = true;
 132:         }
 133:     }
 134:  
 135:  
 136:     /**
 137:      * Stop monitoring changes to the note and note filter.
 138:      */
 139:     private void unregisterReceivers() {
 140:         mObserverRegistered = false;
 141:  
 142:         for (Note note : MoreObjects.firstNonNull(mNotes, new ArrayList<Note>())) {
 143:             note.deleteObserver(this);
 144:         }
 145:  
 146:         mNoteFilterPreferences.unregisterListener(mNoteFilterPreferenceListener);
 147:     }
 148:  
 149:  
 150: }

 


I also set up my EditNoteActivity as an Observer. A loader for that activity is overkill since it only manipulates a single note.  This lets me update the UI to reflect the note, independent of the Objects actually manipulating the note. It dramatically simplifies the logic to try to keep the data and UI consistent across multiple objects.



package com.fsk.mynotes.presentation.activity;
 
 
import android.animation.Animator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
 
import com.fsk.common.presentation.SimpleTextWatcher;
import com.fsk.common.presentation.utils.animations.BackgroundAnimatorHelper;
import com.fsk.common.presentation.utils.animations.SimpleAnimatorListener;
import com.fsk.common.versions.Versions;
import com.fsk.mynotes.R;
import com.fsk.mynotes.constants.NoteColor;
import com.fsk.mynotes.constants.NoteExtraKeys;
import com.fsk.mynotes.data.Note;
import com.fsk.mynotes.presentation.components.NoteEditColorPalette;
import com.fsk.mynotes.presentation.components.NoteEditOptionsBar;
import com.fsk.mynotes.presentation.components.NoteEditToolbar;
import com.fsk.mynotes.services.DeleteNoteService;
import com.fsk.mynotes.services.SaveNoteService;
import com.google.common.base.Preconditions;
 
import java.util.Observable;
import java.util.Observer;
 
import butterknife.ButterKnife;
import butterknife.InjectView;
 
 
/**
 * This activity is responsible for providing the UI to the user that allows them to modify or
 * create a new {@link Note}.
 */
public class EditNoteActivity extends AppCompatActivity implements Observer {
 
    /**
     * Create an intent for this activity that will allow an existing note to be modified.
     *
     * @param context
     *         The context to use for creating the intent.
     * @param note
     *         The note to edit.
     *
     * @return An intent that will start this activity.
     */
    public static Intent createIntentForExistingNote(@NonNull Context context, @NonNull Note note) {
        Preconditions.checkNotNull(context);
        Preconditions.checkNotNull(note);
 
        Intent intent = new Intent(context, EditNoteActivity.class);
        intent.putExtra(NoteExtraKeys.NOTE_KEY, note);
        return intent;
    }
 
 
    /**
     * Create an intent for this activity that will create a new note..
     *
     * @param context
     *         The context to use for creating the intent.
     *
     * @return An intent that will start this activity.
     */
    public static Intent createIntentForNewNote(@NonNull Context context) {
        Preconditions.checkNotNull(context);
        return new Intent(context, EditNoteActivity.class);
    }
 
 
    @InjectView(R.id.activity_single_note_toolbar)
    NoteEditToolbar mToolbar;
 
 
    @InjectView(R.id.activity_single_note_edit_text)
    EditText mEditText;
 
 
    @InjectView(R.id.activity_single_note_edit_container)
    View mNoteContainerView;
 
 
    /**
     * The duration in milliseconds to cross-blend the note colors.
     */
    int mColorShiftDuration;
 
 
    /**
     * The note being edited.
     */
    Note mNote;
 
 
    /**
     * The listener to any text being altered in {@link #mEditText}.
     */
    final TextWatcher mTextWatcher = new SimpleTextWatcher() {
        @Override
        public void afterTextChanged(final Editable s) {
 
            mNote.setText(s.toString());
        }
    };
 
 
    /**
     * A listener to modify the UI coloring when the User changes the note color.
     */
    final NoteEditColorPalette.OnColorSelectedListener mOnColorSelectedListener =
            new NoteEditColorPalette.OnColorSelectedListener() {
 
                @Override
                public void onColorSelected(@NonNull final NoteColor color) {
                    changeColor(color);
                }
            };
 
 
    /**
     * A listener to react to user requests to change the persistency of the note (saving or
     * deleting).
     */
    final NoteEditOptionsBar.OnPersistenceClickListener mOnPersistenceClickListener =
            new NoteEditOptionsBar.OnPersistenceClickListener() {
 
                @Override
                public void onSaveClicked() {
                    saveNote();
                }
 
 
                @Override
                public void onPurgeClicked() {
                    deleteNote();
                }
            };
 
 
    /**
     * A runnable that will update the toolbar.  This exists to allow the update to handle on the UI
     * thread.
     */
    final Runnable mUpdateToolbarRunnable = new Runnable() {
        @Override
        public void run() {
            mToolbar.updateNote(mNote);
        }
    };
 
 
    /**
     * An Animator listener that will finish the activity when the animator completes or is
     * cancelled.
     */
    private final SimpleAnimatorListener mFinishWhenAnimationCompleteListener =
            new SimpleAnimatorListener() {
 
 
                @Override
                public void onAnimationEnd(final Animator animation) {
                    super.onAnimationEnd(animation);
                    safelyFinish();
                }
 
 
                @Override
                public void onAnimationCancel(final Animator animation) {
                    super.onAnimationCancel(animation);
                    safelyFinish();
                }
            };
 
 
    @Override
    protected void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_single_note);
        ButterKnife.inject(this);
 
        mColorShiftDuration = getResources().getInteger(android.R.integer.config_mediumAnimTime);
 
        initializeNote((savedInstanceState == null) ? getIntent().getExtras() : savedInstanceState);
        mNote.addObserver(this);
 
        mToolbar.setOnColorSelectedListener(mOnColorSelectedListener);
        mToolbar.setOnPersistenceClickListener(mOnPersistenceClickListener);
        mToolbar.updateNote(mNote);
 
        changeColor(mNote.getColor());
        mEditText.setText(mNote.getText());
        mEditText.addTextChangedListener(mTextWatcher);
 
        ViewCompat.setTransitionName(mEditText, Long.toString(mNote.getId()));
    }
 
 
    @Override
    protected void onSaveInstanceState(final Bundle outState) {
        super.onSaveInstanceState(outState);
 
        outState.putParcelable(NoteExtraKeys.NOTE_KEY, mNote);
    }
 
 
    @Override
    public void update(final Observable observable, final Object data) {
        mToolbar.post(mUpdateToolbarRunnable);
    }
 
 
    /**
     * Initialize the note based on the bundle data.
     *
     * @param bundle
     *         the bundle containing the note data.
     */
    void initializeNote( Bundle bundle) {
        mNote = null;
 
        //read the note data if the bundle and key exists.
        if ((bundle != null) && bundle.containsKey(NoteExtraKeys.NOTE_KEY)) {
            mNote = bundle.getParcelable(NoteExtraKeys.NOTE_KEY);
        }
 
        /**
         * No note exists, so create a new one.  If anything fails, pack up your toys and go home.
         */
        if (mNote == null) {
            try {
                mNote = new Note.Builder().build();
            }
            catch (Exception e) {
                Toast.makeText(this, R.string.unrecoverable_edit_error, Toast.LENGTH_LONG).show();
                finish();
            }
        }
    }
 
 
    /**
     * Animate the UI color change based on the note colors.
     *
     * @param newColor
     *         the new color for the note.
     */
    void changeColor(@NonNull NoteColor newColor) {
        NoteColor oldColor = mNote.getColor();
        mNote.setColor(newColor);
 
        //change the color of the edit text.
        BackgroundAnimatorHelper.crossBlendColorResource(mEditText, oldColor.colorResourceId,
                                                         newColor.colorResourceId, 0,
                                                         mColorShiftDuration, null);
 
        //change the border color around the edit text.
        BackgroundAnimatorHelper
                .crossBlendColorResource(mNoteContainerView, oldColor.darkColorResourceId,
                                         newColor.darkColorResourceId, 0, mColorShiftDuration,
                                         null);
 
        //change the toolbar color.
        BackgroundAnimatorHelper.crossBlendColorResource(mToolbar, oldColor.darkColorResourceId,
                                                         newColor.darkColorResourceId, 0,
                                                         mColorShiftDuration, null);
    }
 
 
    /**
     * Start the save note service and then finish the activity.
     */
    void saveNote() {
        SaveNoteService.startService(this, mNote);
        safelyFinish();
    }
 
 
    /**
     * Start the delete note service and animate the note deletion.  The activity will finish when
     * the animation completes.
     */
    void deleteNote() {
 
        NoteColor noteColor = mNote.getColor();
        BackgroundAnimatorHelper
                .crossBlendColorResource(mNoteContainerView, noteColor.darkColorResourceId,
                                         android.R.color.transparent, 0, mColorShiftDuration, null);
        mEditText.animate().alpha(0f).setStartDelay(mColorShiftDuration)
                 .setDuration(mColorShiftDuration).setListener(mFinishWhenAnimationCompleteListener)
                 .start();
 
        DeleteNoteService.startService(this, mNote);
    }
 
 
    /**
     * Safely finish this activity.  If the build is at least lollipop, then the transition is
     * allowed to occur before the finish.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void safelyFinish() {
        if (Versions.isAtLeastLollipop()) {
            finishAfterTransition();
        }
        else {
            finish();
        }
    }
}

Commits


The majority of the  changes can be found on github at https://github.com/fsk-software/mynotes/commit/ed209572c5e1302ac03288a84b4e2003cb5894e7.